Source code for mplsoccer.py_pizza

"""A Python module for plotting pizza-plots.

Author: Anmol_Durgapal(@slothfulwave612)

The idea is inspired by Tom Worville, Football Slices, Soma Zero FC and Soumyajit Bose.
"""

import matplotlib.pyplot as plt
import numpy as np

__all__ = ["PyPizza"]


[docs] class PyPizza: """A class for plotting pizza charts in Matplotlib. Parameters ---------- params : sequence of str The name of parameters (e.g. 'Key Passes') min_range, max_range : sequence of floats, default None Minimum and maximum range for each parameter background_color : str, default "#F2F2F2" The background-color of the plot. inner_circle_size : float, default 5.0 Size of the inner circle. straight_line_limit : float, default 100.0 Limit till which straight line will go. straight_line_color : str, default "#808080" Color for the straight-lines. straight_line_lw : float, default 2.0 Linewidth for the straight-lines. straight_line_ls : str, default '-' Linestyle for the straight-lines. last_circle_color : str, default "#000000" Color for the last circle. last_circle_lw : float, default 2.0 Linewidth for the last circle. last_circle_ls : str, default '-' Linestyle for the last circle. other_circle_color : str, default "#808080" Color for other circles. other_circle_lw : float, default 2.0 Linewidth for other circle. other_circle_ls : str, default "--" Linestyle for other circle. """ def __init__(self, params, min_range=None, max_range=None, background_color="#F2F2F2", inner_circle_size=5.0, straight_line_limit=100.0, straight_line_color="#808080", straight_line_lw=2.0, straight_line_ls='-', last_circle_color="#000000", last_circle_lw=2.0, last_circle_ls='-', other_circle_color="#808080", other_circle_lw=2.0, other_circle_ls="--"): self.params = params self.min_range = min_range self.max_range = max_range self.background_color = background_color self.inner_circle_size = inner_circle_size self.straight_line_limit = straight_line_limit self.straight_line_color = straight_line_color self.straight_line_lw = straight_line_lw # if any of the linewidths are zero set the linestyle to solid # to prevent https://github.com/andrewRowlinson/mplsoccer/issues/71 if straight_line_lw == 0: self.straight_line_ls = 'solid' else: self.straight_line_ls = straight_line_ls self.last_circle_color = last_circle_color self.last_circle_lw = last_circle_lw if last_circle_lw == 0: self.last_circle_ls = 'solid' else: self.last_circle_ls = last_circle_ls self.other_circle_color = other_circle_color self.other_circle_lw = other_circle_lw if other_circle_lw == 0: self.other_circle_ls = 'solid' else: self.other_circle_ls = other_circle_ls self.param_texts = [] self.value_texts = [] self.compare_value_texts = [] self.theta = None # filled-in by make_pizza method def __repr__(self): return (f'{self.__class__.__name__}(' f'params={self.params}, ' f'min_range={self.min_range}, ' f'max_range={self.max_range}, ' f'background_color={self.background_color}, ' f'inner_circle_size={self.inner_circle_size}, ' f'straight_line_limit={self.straight_line_limit}, ' f'straight_line_color={self.straight_line_color}, ' f'straight_line_lw={self.straight_line_lw}, ' f'straight_line_ls={self.straight_line_ls}, ' f'last_circle_color={self.last_circle_color}, ' f'last_circle_lw={self.last_circle_lw}, ' f'last_circle_ls={self.last_circle_ls}, ' f'other_circle_color={self.other_circle_color}, ' f'other_circle_lw={self.other_circle_lw}, ' f'other_circle_ls={self.other_circle_ls}, ')
[docs] def make_pizza(self, values, compare_values=None, bottom=0.0, figsize=(24, 16), ax=None, param_location=108, slice_colors=None, value_colors=None, compare_colors=None, value_bck_colors=None, compare_value_colors=None, compare_value_bck_colors=None, color_blank_space=None, blank_alpha=0.5, kwargs_slices=None, kwargs_compare=None, kwargs_params=None, kwargs_values=None, kwargs_compare_values=None): """To make the pizza plot. Parameters ---------- values : sequence of floats/int Values for each parameter. compare_values : sequence of floats/int, default None Comparison Values for each parameter. bottom : float, default 0.0 Start value for the bar. figsize : tuple of floats, default (24, 16) The figure size in inches (width, height). ax : matplotlib axis, default None matplotlib.axes.Axes. If None is specified the pitch is plotted on a new figure. param_location : float, default 108 Location where params will be added. slice_colors : sequence of str, default None Color for individual slices. value_colors : sequence of str, default None Color for the individual values-text. compare_colors : sequence of str, default None Color for the individual comparison-slices. value_bck_colors : sequence of str, default None Color for background text-box for individual value-text. compare_value_colors : sequence of str, default None Color for the individual comparison-values-text. compare_value_bck_colors : sequence of str, default None Color for background text-box for individual comparison-value-text. color_blank_space : str/sequence of str, default None. To color the blank space area in the plot. if "same" --> same color as main-slices if sequence of str --> colors from the defined sequence blank_alpha : float, default 0.5 Alpha value for blank-space-colors **kwargs_slices : All keyword arguments are passed on to axes.Axes.bar for slices. **kwargs_compare : All keyword arguments are passed on to axes.Axes.bar for comparison-slices. **kwargs_params : All keyword arguments are passed on to axes.Axes.text for adding parameters. **kwargs_values : All keyword arguments are passed on to axes.Axes.text for adding values. **kwargs_compare_values : All keyword arguments are passed on to axes.Axes.text for adding comparison-values. Returns ------- If ax=None returns a matplotlib Figure and Axes. Else the settings are applied on an existing axis and returns None. """ if len(self.params) != len(values): raise Exception("Length of params and values are not equal!!!") if slice_colors is not None and len(slice_colors) != len(self.params): raise Exception("Length of slice_colors and params are not equal!!!") if value_colors is not None and len(value_colors) != len(self.params): raise Exception("Length of text_colors and params are not equal!!!") if value_bck_colors is not None and len(value_bck_colors) != len(self.params): raise Exception("Length of text_bck_colors and params are not equal!!!") if compare_value_bck_colors is not None and len(compare_value_bck_colors) != len(values): raise Exception("Length of compare_value_bck_colors and values are not equal!!!") if value_bck_colors is not None and len(value_bck_colors) != len(self.params): raise Exception("Length of text_bck_colors and params are not equal!!!") if self.min_range is not None and len(self.min_range) != len(self.max_range): raise Exception("Length of min_range and max_range are not equal!!!") if self.min_range is not None and len(self.min_range) != len(values): raise Exception("Length of min_range and values are not equal!!!") if isinstance(color_blank_space, list) and len(color_blank_space) != len(self.params): raise Exception("Length of color_blank_space and params are not equal!!!") # set empty dict if None if kwargs_slices is None: kwargs_slices = dict() if kwargs_compare is None: kwargs_compare = dict() if kwargs_params is None: kwargs_params = dict() if kwargs_values is None: kwargs_values = dict() if kwargs_compare_values is None: kwargs_compare_values = dict() if ax is None: fig, ax = plt.subplots( figsize=figsize, facecolor=self.background_color, subplot_kw={'projection': 'polar'} ) ax.set_facecolor(self.background_color) return_fig_ax = True else: return_fig_ax = False # total number of attributes total_params = len(self.params) # calculate theta value and width of the bar self.theta, width = np.linspace( 0.0, 2 * np.pi, total_params, endpoint=False, retstep=True ) if self.min_range is not None and self.max_range is not None: self.min_range = np.array(self.min_range) self.max_range = np.array(self.max_range) temp_values = self.__get_value(values) else: temp_values = values # plot slice for values main_slice = ax.bar( x=self.theta, height=temp_values, width=width, bottom=bottom, **kwargs_slices ) # color individual slices if slice_colors is not None: for index, slices in enumerate(main_slice): slices.set_facecolor(slice_colors[index]) # color blank area if color_blank_space is not None: blank_space = ax.bar( self.theta, height=self.straight_line_limit, width=width, bottom=bottom, zorder=main_slice[0].get_zorder()-1 ) if color_blank_space == "same": for index, (blank, slice_) in enumerate(zip(blank_space, main_slice)): blank.set_facecolor(slice_.get_facecolor()) blank.set_alpha(blank_alpha) else: for blank, color in zip(blank_space, color_blank_space): blank.set_facecolor(color) blank.set_alpha(blank_alpha) # add comparison values if compare_values is not None: if self.min_range is not None and self.max_range is not None: temp_compare_values = self.__get_value(compare_values) else: temp_compare_values = compare_values compare_slice = ax.bar( x=self.theta, height=temp_compare_values, width=width, bottom=bottom, **kwargs_compare ) for idx, (slice_c, slice_m) in enumerate(zip(compare_slice, main_slice)): if temp_values[idx] <= temp_compare_values[idx]: slice_c.set_zorder(slice_m.get_zorder() - 0.1) if temp_values[idx] > temp_compare_values[idx]: slice_c.set_zorder(slice_m.get_zorder() + 0.1) # color individual slices if compare_colors is not None: for index, slices in enumerate(compare_slice): slices.set_facecolor(compare_colors[index]) else: temp_compare_values = None # setup-pizza self.__setup_pizza(ax, width) # add text self.__add_texts( ax, values, param_location, value_colors=value_colors, value_bck_colors=value_bck_colors, compare_values=compare_values, compare_value_colors=compare_value_colors, temp_values=temp_values, temp_compare_values=temp_compare_values, compare_value_bck_colors=compare_value_bck_colors, kwargs_params=kwargs_params, kwargs_values=kwargs_values, kwargs_compare_values=kwargs_compare_values ) if return_fig_ax: return fig, ax return None
def __setup_pizza(self, ax, width): """To set up the pizza plot. Parameters ---------- ax : matplotlib axis. matplotlib.axes.Axes. width : sequence of float. width of the slices. """ # degrees gone ax.tick_params(labelbottom=False) # inner circle size ax.set_rorigin(-self.inner_circle_size) # values off ax.set_yticklabels([]) ax.set_xticklabels([]) # start from top and to the right ax.set_theta_zero_location('N') ax.set_theta_direction(-1) # set up line for each bar ax.set_thetagrids((self.theta+width/2) * 180 / np.pi) # last circle off ax.spines['polar'].set_visible(False) # set limit for straight line ax.set_rmax(self.straight_line_limit) # for last circle index = -1 gridlines = ax.yaxis.get_gridlines() gridlines[index].set_color(self.last_circle_color) gridlines[index].set_linewidth(self.last_circle_lw) gridlines[index].set_linestyle(self.last_circle_ls) # for other circles excluding last circle for i in list(ax.yaxis.get_gridlines())[:-1]: i.set_color(self.other_circle_color) i.set_linewidth(self.other_circle_lw) i.set_linestyle(self.other_circle_ls) # for other straight-line for i in list(ax.xaxis.get_gridlines()): i.set_color(self.straight_line_color) i.set_linewidth(self.straight_line_lw) i.set_linestyle(self.straight_line_ls) def __add_texts(self, ax, values, param_location, temp_values=None, temp_compare_values=None, value_colors=None, value_bck_colors=None, compare_values=None, compare_value_colors=None, compare_value_bck_colors=None, kwargs_params=None, kwargs_values=None, kwargs_compare_values=None): """To make the pizza plot. Parameters ---------- ax : matplotlib axis. matplotlib.axes.Axes. values : sequence of floats/int Values for each parameter. param_location : float, default 108 Location where params will be added. temp_values : sequence of floats/int Values for each parameter (if ranges are specified) temp_compare_values : sequence of floats/int Comparison-Values for each parameter (if ranges are specified) value_colors : sequence of str, default None Color for the individual values-text. value_bck_colors : sequence of str, default None Color for background text-box for individual value-text. compare_values : sequence of floats/int, default None Comparison Values for each parameter. compare_value_colors : sequence of str, default None Color for the individual comparison-values-text. compare_value_bck_colors : sequence of str, default None Color for background text-box for individual comparison-value-text. **kwargs_params : All keyword arguments are passed on to axes.Axes.text for adding parameters. **kwargs_values : All keyword arguments are passed on to axes.Axes.text for adding values. **kwargs_compare_values : All keyword arguments are passed on to axes.Axes.text for adding comparison-values. Returns ------- If ax=None returns a matplotlib Figure and Axes. Else the settings are applied on an existing axis and returns None. """ # set to empty dict if None if kwargs_params is None: kwargs_params = dict() if kwargs_values is None: kwargs_values = dict() if kwargs_compare_values is None: kwargs_compare_values = dict() # total length of parameters total_params = len(self.params) # get the rotation angles rotation = (2 * np.pi / total_params) * np.arange(total_params) # flip the rotation if the label is in lower half mask_flip_label = (rotation > np.pi / 2) & (rotation < np.pi / 2 * 3) rotation[mask_flip_label] = rotation[mask_flip_label] + np.pi rotation_degrees = -np.rad2deg(rotation) # plot params for x, rotation, label in zip(self.theta, rotation_degrees, self.params): temp_text = ax.text( x, param_location, label, rotation=rotation, rotation_mode="anchor", ha="center", **kwargs_params ) self.param_texts.append(temp_text) # plot values for i, (x, value, rotation) in enumerate(zip(self.theta, values, rotation_degrees)): if value_colors is not None: kwargs_values["color"] = value_colors[i] if value_bck_colors is not None and kwargs_values.get("bbox") is not None: kwargs_values["bbox"]["facecolor"] = value_bck_colors[i] temp_text = ax.text( x, temp_values[i], value, ha="center", **kwargs_values ) self.value_texts.append(temp_text) # plot comparison values if compare_values is not None: for i, (x, value, rotation) in enumerate(zip(self.theta, compare_values, rotation_degrees)): if compare_value_colors is not None: kwargs_compare_values["color"] = compare_value_colors[i] if compare_value_bck_colors is not None and kwargs_values.get("bbox") is not None: kwargs_compare_values["bbox"]["facecolor"] = compare_value_bck_colors[i] if temp_compare_values is not None: value_1 = temp_compare_values[i] value_2 = value else: value_1 = value_2 = value temp_text = ax.text( x, value_1, value_2, ha="center", **kwargs_compare_values ) self.compare_value_texts.append(temp_text) def __get_value(self, values): """To get values if ranges are passed.""" label_range = np.abs(self.max_range - self.min_range) range_min = np.minimum(self.min_range, self.max_range) range_max = np.maximum(self.min_range, self.max_range) values_clipped = np.minimum(np.maximum(values, range_min), range_max) proportion = np.abs(values_clipped - self.min_range) / label_range vertices = (proportion * 100) return vertices
[docs] def adjust_texts(self, params_offset, offset=0.0, adj_comp_values=False): """ To adjust the value-texts. (if they are overlapping) Parameters ---------- params_offset : sequence of bool Pass True for parameter whose value are to be adjusted. offset : float, default 0.0 The value will define how much adjustment will be made. adj_comp_values : bool, defaults False To make adjustment for comparison-values-text. """ if len(params_offset) != len(self.params): raise Exception("Length of params_offset and params are not equal!!!") # fetch index where value is True idx_value = [i for i, x in enumerate(params_offset) if x] if adj_comp_values: texts = self.get_compare_value_texts() else: texts = self.get_value_texts() # iterate over text objects and adjust the text for which params_offset is True for count, (temp_text, theta) in enumerate( zip(texts, self.get_theta()) ): # fetch the value adj_val = offset if count in idx_value else 0.0 # adjust the position # add some value to x-coordinate and keep y-coordinate same temp_text.set_position(( theta+adj_val, temp_text.get_position()[1] ))
[docs] def get_param_texts(self): """To fetch list of axes.text for params.""" return self.param_texts
[docs] def get_value_texts(self): """To fetch list of axes.text for values.""" return self.value_texts
[docs] def get_compare_value_texts(self): """To fetch list of axes.text for comparison-values.""" return self.compare_value_texts
[docs] def get_theta(self): """To fetch list containing theta values (x-coordinate for each text).""" return self.theta
# __str__ is the same as __repr__ __str__ = __repr__