Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Images to Bars in Race #25

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

Make animated bar and line chart races in Python with matplotlib or plotly.


## Bar Chart Race with Images For Bars
This is what this feature should accomplish. It is a little rough right now and not very generalizable.
![img](gif_for_github.gif)


![img](https://github.com/dexplo/bar_chart_race/raw/gh-pages/images/covid19_horiz.gif)

## Official Documentation
Expand Down
197 changes: 191 additions & 6 deletions bar_chart_race/_bar_chart_race.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,53 @@
from ._func_animation import FuncAnimation
from matplotlib.colors import Colormap

from matplotlib.offsetbox import OffsetImage,AnnotationBbox,TextArea
from PIL import Image #for opening images
#import matplotlib.image as mpimg
import os

from ._common_chart import CommonChart
from ._utils import prepare_wide_data



def get_image_label(root_folder,name):
#path = "data/flags/Flags/flags/flags/24/{}.png".format(name.title())
path = os.path.join(root_folder , name)
#im = plt.imread(path)
img = Image.open(path)
img.thumbnail((200,200),Image.ANTIALIAS)
return img

def get_image_name(col_name):
'''
Needs to account for more cases. This is incomplete but it will do for now. It assumes there
is only one `.` in the filename. If it finds a dot, it will assume the name already has an extension.
If not, it will append `.png` to the name.
col_name: str
Takes a column or bar name and attaches an image extension (only .png for now)

Returns
----------
str
'''
split_name = col_name.split('.')
if len(split_name) > 1:
img_name = split_name
else:
img_name = col_name + '.png'
return img_name


class _BarChartRace(CommonChart):

def __init__(self, df, filename, orientation, sort, n_bars, fixed_order, fixed_max,
steps_per_period, period_length, end_period_pause, interpolate_period,
period_label, period_template, period_summary_func, perpendicular_bar_func,
colors, title, bar_size, bar_textposition, bar_texttemplate, bar_label_font,
tick_label_font, tick_template, shared_fontdict, scale, fig, writer,
bar_kwargs, fig_kwargs, filter_column_colors):
bar_kwargs, fig_kwargs, filter_column_colors,
img_label_folder,tick_label_mode,tick_image_mode):
self.filename = filename
self.extension = self.get_extension()
self.orientation = orientation
Expand Down Expand Up @@ -58,6 +94,12 @@ def __init__(self, df, filename, orientation, sort, n_bars, fixed_order, fixed_m
self.subplots_adjust = self.get_subplots_adjust()
self.fig = self.get_fig(fig)

self.img_label_folder = img_label_folder #root folder where image labels are stored
self.tick_label_mode = tick_label_mode
self.tick_image_mode = tick_image_mode
self.img_label_artist = [] #stores image artists


def validate_params(self):
if isinstance(self.filename, str):
if '.' not in self.filename:
Expand Down Expand Up @@ -142,6 +184,107 @@ def get_font(self, font, ticks=False):
font = {**default_font_dict, **font}
return font

def offset_image(self,location,lenght,name,ax):
"""
Creates AnnotationBbox objects to display image labels on the graph. There is a
new AnnotationBbox object created at each frame. Maybe a better approach is to create
the original AnnotationBbox objects and then simply update them as the program runs.

Parameters
----------
location: scalar or array
Coordinate of the bar (in the axis that is moving)
name: str
Filename of image file stored in `self.img_label_folder`

"""
#load image as an OffsetImage object
img_name = get_image_name(name)
img = get_image_label(self.img_label_folder,img_name)
im = OffsetImage(img,zoom=.08)
im.image.axes = ax


if self.orientation=='h':
ab = AnnotationBbox(im,(lenght,location,),xybox=(0,0.),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)
if self.tick_label_mode=='mixed':
#load text as TextArea object
label_text = TextArea(name)
text_ab = AnnotationBbox(label_text,(0,location,),xybox=(-30,-5),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)
self.img_label_artist.append(text_ab)
ax.add_artist(text_ab)

elif self.orientation=='v':
ab = AnnotationBbox(im,(location,lenght,),xybox=(0.,0),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)
if self.tick_label_mode=='mixed':
#load text as TextArea object
label_text = TextArea(name)
text_ab = AnnotationBbox(label_text,(location,0,),xybox=(-5,30),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)
self.img_label_artist.append(text_ab)
ax.add_artist(text_ab)

self.img_label_artist.append(ab)
ax.add_artist(ab)

def _add_tick_label_offset_image(self,location,length,name,ax):
"""
Creates AnnotationBbox objects to display image labels on the graph. There is a
new AnnotationBbox object created at each frame. Maybe a better approach is to create
the original AnnotationBbox objects and then simply update them as the program runs.

Parameters
----------
location: scalar or array
Coordinate of the bar (in the axis that is moving)
name: str
Filename of image file stored in `self.img_label_folder`

"""
#load image as an OffsetImage object
img_name = get_image_name(name)
img = get_image_label(self.img_label_folder,img_name)
im = OffsetImage(img,zoom=.08)
im.image.axes = ax

#renderer = self.fig.canvas.renderer
#_,_,img_width,img_height = im.get_window_extent(renderer=None)
#print(im.get_data())
img_width = 30
img_height = 30

if self.tick_image_mode=='trailing': #images move along with the bar
if self.orientation=='h':
#len_bar = (img_width/2) + 2 if length < img_width else length - (img_width/2) - 2
len_bar = length
xybox_val = (15,0) if length < 30 else (-5,0)
ab = AnnotationBbox(im,(len_bar,location,),xybox=xybox_val,frameon=False,xycoords='data',
boxcoords='offset points',pad=0)
else:
#len_bar = (img_height/2) + 2 if length < img_height else length - (img_height/2) - 2
len_bar = length
xybox_val = (0,15) if length < 30 else (0,-5)
ab = AnnotationBbox(im,(location,len_bar,),xybox=(0.,-10.),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)

elif self.tick_image_mode=='fixed': #images stay fixed at the beginning of the bar
if self.orientation=='h':
#len_bar = (img_width/2) + 2
len_bar = 10
ab = AnnotationBbox(im,(len_bar,location,),xybox=(10,0.),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)
else:
#len_bar = (img_height/2) + 2
len_bar = 10
ab = AnnotationBbox(im,(location,len_bar,),xybox=(0.,10.),frameon=False,xycoords='data',
boxcoords='offset points',pad=0)

self.img_label_artist.append(ab)
ax.add_artist(ab)

def prepare_data(self, df):
if self.fixed_order is True:
last_values = df.iloc[-1].sort_values(ascending=False)
Expand Down Expand Up @@ -350,7 +493,9 @@ def plot_bars(self, ax, i):
if self.orientation == 'h':
ax.barh(bar_location, bar_length, tick_label=cols,
color=colors, **self.bar_kwargs)
ax.set_yticklabels(ax.get_yticklabels(), **self.tick_label_font)
ax.set_yticklabels(ax.get_yticklabels(), **self.tick_label_font,wrap=True)#,visible=False)
#ax.set_yticklabels([])
#ax.tick_params(top=False, bottom=False, left=False, right=False, labelleft=True, labelbottom=True)
if not self.fixed_max and self.bar_textposition == 'outside':
max_bar = bar_length.max()
new_max_pixels = ax.transData.transform((max_bar, 0))[0] + self.extra_pixels
Expand All @@ -366,6 +511,12 @@ def plot_bars(self, ax, i):
new_ymax = ax.transData.inverted().transform((0, new_max_pixels))[1]
ax.set_ylim(ax.get_ylim()[0], new_ymax)

if self.img_label_folder: #here I am handling the addition of images as the bar tick labels
zipped = zip(bar_location,bar_length,cols)
for bar_loc,bar_len,col_name in zipped:
#self.offset_image(bar_loc,bar_len,col_name,ax)
self._add_tick_label_offset_image(bar_loc,bar_len,col_name,ax)

self.set_major_formatter(ax)
self.add_period_label(ax, i)
self.add_period_summary(ax, i)
Expand Down Expand Up @@ -425,7 +576,6 @@ def add_bar_labels(self, ax, bar_location, bar_length):
text = self.bar_texttemplate.format(x=val)

xtext, ytext = ax.transLimits.inverted().transform((xtext, ytext))

text_obj = ax.text(xtext, ytext, text, clip_on=True, **self.bar_label_font)
text_objs.append(text_obj)
return text_objs
Expand All @@ -452,6 +602,7 @@ def add_perpendicular_bar(self, ax, bar_length, i):
line.set_ydata([val] * 2)

def anim_func(self, i):

if i is None:
return
ax = self.fig.axes[0]
Expand All @@ -460,12 +611,20 @@ def anim_func(self, i):
start = int(bool(self.period_label))
for text in ax.texts[start:]:
text.remove()

if self.img_label_folder:
for artist in self.img_label_artist:
artist.remove()
self.img_label_artist = [] #clears the list of artists for the next cycle.
self.plot_bars(ax, i)
self.fig.tight_layout()


def make_animation(self):
def init_func():
ax = self.fig.axes[0]
self.plot_bars(ax, 0)
self.fig.tight_layout()

interval = self.period_length / self.steps_per_period
pause = int(self.end_period_pause // interval)
Expand Down Expand Up @@ -516,8 +675,9 @@ def bar_chart_race(df, filename=None, orientation='h', sort='desc', n_bars=None,
perpendicular_bar_func=None, colors=None, title=None, bar_size=.95,
bar_textposition='outside', bar_texttemplate='{x:,.0f}',
bar_label_font=None, tick_label_font=None, tick_template='{x:,.0f}',
shared_fontdict=None, scale='linear', fig=None, writer=None,
bar_kwargs=None, fig_kwargs=None, filter_column_colors=False):
shared_fontdict=None, scale='linear', fig=None, writer=None, bar_kwargs=None,
fig_kwargs=None, filter_column_colors=False,
img_label_folder=None,tick_label_mode='image',tick_image_mode='trailing'):
'''
Create an animated bar chart race using matplotlib. Data must be in
'wide' format where each row represents a single time period and each
Expand Down Expand Up @@ -815,7 +975,31 @@ def func(val):
EXPERIMENTAL
This parameter is experimental and may be changed/removed
in a later version.

img_label_folder : `None` or str, default `None`
Folder that contains images to be used as labels in the chart.
The folder should contain one image per bar in the chart and
the filenames should match name of the corresponding column in the dataframe.

tick_label_mode : str, default `image`
Dictates what kind of tick label will be used for the bars. Depending on the
mode selected, only the image might show up, or both image and text.
For only text, simply use the default value of `None` for `img_label_folder` above.

Possible keys are:
`image`, `mixed`

DO NOT USE. I have not polished it and it does not look good. Just let it use the
default value `image` and everything will be fine.

tick_image_mode : str, default `trailing`
Tells how to update image tick labels. `trailing` will make it so that the image is
always moving with the bar as it grows. `fixed` will keep the image at a fixed
location near the start of the bar. I have not decided on the best way to automate
the location.

Possible keys are:
`trailing`, `fixed`
Returns
-------
When `filename` is left as `None`, an HTML5 video is returned as a string.
Expand Down Expand Up @@ -872,5 +1056,6 @@ def func(val):
period_label, period_template, period_summary_func, perpendicular_bar_func,
colors, title, bar_size, bar_textposition, bar_texttemplate,
bar_label_font, tick_label_font, tick_template, shared_fontdict, scale,
fig, writer, bar_kwargs, fig_kwargs, filter_column_colors)
fig, writer, bar_kwargs, fig_kwargs, filter_column_colors,
img_label_folder,tick_label_mode,tick_image_mode)
return bcr.make_animation()
20 changes: 20 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ df.bcr.bar_chart_race()

<div>{{ video('basic_default') }}</div>

### Add Images to Bars
Images can be added as icons for each bar by using the parameter `img_label_folder` in the constructor. `img_label_folder` is a string containing the path to the folder where the images are stored.

The folder you indicate should contain one image per bar, and the filename should match the name of the columns in the dataset.

An additional parameter `tick_image_mode` can be used to control how the images update with respect to the bars by passing the string `trailing` or `fixed`. In trailing mode the images will stay at the end of their respective bars. In fixed mode, images will stay

```python
import bar_chart_race as bcr
import pandas as pd

df = bcr.load_dataset('covid19_tutorial')

folder_with_images = "flag_images" #have a folder with images ready

bcr.bar_chart_race(df,
img_label_folder='bar_image_labels',
tick_image_mode='trailing',)
```

### Vertical bars

By default, bars are horizontal. Use the `orientation` parameter to switch to vertical.
Expand Down
Binary file added gif_for_github.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.