jonboy
jonboy

Reputation: 366

Animate label with bar chart - matplotlib

The code below animates a bar chart and associated label values. The issue I'm having is positioning the label when the integer is negative. Specifically, I want the label to be positioned on top of the bar, not inside it. It's working for the first frame but the subsequent frames of animation revert back to plotting the label inside the bar chart for negative integers.

def autolabel(rects, ax):

    # Get y-axis height to calculate label position from.
    ts = []

    (y_bottom, y_top) = ax.get_ylim()
    y_height = y_top - y_bottom

    for rect in rects:

        height = 0
        if rect.get_y() < 0:
            height = rect.get_y()
        else:
            height = rect.get_height()

        p_height = (height / y_height)

        if p_height > 0.95: 
            label_position = height - (y_height * 0.05) if (height > -0.01) else height + (y_height * 0.05) 
        else:
            label_position = height + (y_height * 0.01) if (height > -0.01) else height - (y_height * 0.05) 

        t = ax.text(rect.get_x() + rect.get_width() / 2., label_position,
                '%d' % int(height),
                ha='center', va='bottom')
    
        ts.append(t)
    
    return ts

def gradientbars(bars, ax, cmap, vmin, vmax):

    g = np.linspace(vmin,vmax,100)
    grad = np.vstack([g,g]).T
    xmin,xmax = ax.get_xlim()
    ymin,ymax = ax.get_ylim()
    ims = []

    for bar in bars:
        bar.set_facecolor('none')
        im = ax.imshow(grad, aspect="auto", zorder=0, cmap=cmap, vmin=vmin, vmax=vmax, extent=(xmin,xmax,ymin,ymax))
        im.set_clip_path(bar)
        ims.append(im)
    
    return ims

vmin = -6
vmax = 6
cmap = 'PRGn'

data = np.random.randint(-5,5, size=(10, 4))

x = [chr(ord('A')+i) for i in range(4)]

fig, ax = plt.subplots()
ax.grid(False)
ax.set_ylim(vmin, vmax)
rects = ax.bar(x,data[0])

labels = autolabel(rects, ax)
imgs = gradientbars(rects, ax, cmap=cmap, vmin=vmin, vmax=vmax)

def animate(i):
    for rect,label,img,yi in zip(rects, labels, imgs, data[i]):
        rect.set_height(yi)
        label.set_text('%d'%int(yi))
        label.set_y(yi)
        img.set_clip_path(rect)

anim = animation.FuncAnimation(fig, animate, frames = len(data), interval = 500)
plt.show()

Upvotes: 0

Views: 174

Answers (1)

Ynjxsjmh
Ynjxsjmh

Reputation: 30050

It's working for the first frame.

You call autolabel(rects, ax) in the first plot, so the label is well placed.

The subsequent frames of animation revert back to plotting the label inside the bar chart for negative integers.

The label position of subsequent frames is set by label.set_y(yi). yi is from data[i], you didn't consider the negative value here.

I create a function named get_label_position(height) to calculate the right label position for give height. It uses a global variable y_height. And call this function before label.set_y().

import matplotlib.pyplot as plt
from matplotlib import animation
import pandas as pd
import numpy as np


def get_label_position(height):
    p_height = (height / y_height)

    label_position = 0
    if p_height > 0.95: 
        label_position = height - (y_height * 0.05) if (height > -0.01) else height + (y_height * 0.05)
    else:
        label_position = height + (y_height * 0.01) if (height > -0.01) else height - (y_height * 0.05)

    return label_position


def autolabel(rects, ax):

    # Get y-axis height to calculate label position from.
    ts = []

    (y_bottom, y_top) = ax.get_ylim()
    y_height = y_top - y_bottom

    for rect in rects:

        height = 0
        if rect.get_y() < 0:
            height = rect.get_y()
        else:
            height = rect.get_height()

        p_height = (height / y_height)

        if p_height > 0.95: 
            label_position = height - (y_height * 0.05) if (height > -0.01) else height + (y_height * 0.05) 
        else:
            label_position = height + (y_height * 0.01) if (height > -0.01) else height - (y_height * 0.05) 

        t = ax.text(rect.get_x() + rect.get_width() / 2., label_position,
                '%d' % int(height),
                ha='center', va='bottom')
    
        ts.append(t)
    
    return ts

def gradientbars(bars, ax, cmap, vmin, vmax):
    g = np.linspace(vmin,vmax,100)
    grad = np.vstack([g,g]).T
    xmin,xmax = ax.get_xlim()
    ymin,ymax = ax.get_ylim()
    ims = []

    for bar in bars:
        bar.set_facecolor('none')
        im = ax.imshow(grad, aspect="auto", zorder=0, cmap=cmap, vmin=vmin, vmax=vmax, extent=(xmin,xmax,ymin,ymax))
        im.set_clip_path(bar)
        ims.append(im)
    
    return ims

vmin = -6
vmax = 6
cmap = 'PRGn'

data = np.random.randint(-5,5, size=(10, 4))

x = [chr(ord('A')+i) for i in range(4)]

fig, ax = plt.subplots()
ax.grid(False)
ax.set_ylim(vmin, vmax)
rects = ax.bar(x,data[0])

labels = autolabel(rects, ax)
imgs = gradientbars(rects, ax, cmap=cmap, vmin=vmin, vmax=vmax)

(y_bottom, y_top) = ax.get_ylim()
y_height = y_top - y_bottom

def animate(i):
    for rect,label,img,yi in zip(rects, labels, imgs, data[i]):
        rect.set_height(yi)
        label.set_text('%d'%int(yi))
        label.set_y(get_label_position(yi))
        img.set_clip_path(rect)


anim = animation.FuncAnimation(fig, animate, frames = len(data), interval = 500)
plt.show()

Upvotes: 1

Related Questions