def lines(self, xstart, ystart, xend, yend, color=None, n_segments=100, comet=False, transparent=False, alpha_start=0.01, alpha_end=1, cmap=None, ax=None, **kwargs): validate_ax(ax) linecollection = lines(xstart, ystart, xend, yend, color=color, n_segments=n_segments, comet=comet, transparent=transparent, alpha_start=alpha_start, alpha_end=alpha_end, cmap=cmap, ax=ax, vertical=self.vertical, reverse_cmap=self.reverse_cmap, **kwargs) return linecollection
def plot(self, x, y, ax=None, **kwargs): """ Utility wrapper around matplotlib.axes.Axes.plot, which automatically flips the x and y coordinates if the pitch is vertical. Parameters ---------- x, y : array-like or scalar. Commonly, these parameters are 1D arrays. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.plot. Returns ------- lines : A list of Line2D objects representing the plotted data. Examples -------- >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.plot([30, 35, 20], [30, 19, 40], ax=ax) """ validate_ax(ax) x, y = self._reverse_if_vertical(x, y) return ax.plot(x, y, **kwargs)
def label_heatmap(self, stats, ax=None, **kwargs): """ Labels the heatmap(s) and automatically flips the coordinates if the pitch is vertical. Parameters ---------- stats : A dictionary or list of dictionaries. This should be calculated via bin_statistic_positional() or bin_statistic(). ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.annotate. Returns ------- annotations : A list of matplotlib.text.Annotation. Examples -------- >>> from mplsoccer import Pitch >>> import numpy as np >>> import matplotlib.patheffects as path_effects >>> pitch = Pitch(line_zorder=2, pitch_color='black') >>> fig, ax = pitch.draw() >>> x = np.random.uniform(low=0, high=120, size=100) >>> y = np.random.uniform(low=0, high=80, size=100) >>> stats = pitch.bin_statistic(x, y) >>> pitch.heatmap(stats, edgecolors='black', cmap='hot', ax=ax) >>> stats['statistic'] = stats['statistic'].astype(int) >>> path_eff = [path_effects.Stroke(linewidth=0.5, foreground='#22312b')] >>> text = pitch.label_heatmap(stats, color='white', ax=ax, fontsize=20, ha='center', \ va='center', path_effects=path_eff) """ validate_ax(ax) if not isinstance(stats, list): stats = [stats] annotation_list = [] for bin_stat in stats: # remove labels outside the plot extents mask_x_outside1 = bin_stat['cx'] < self.dim.pitch_extent[0] mask_x_outside2 = bin_stat['cx'] > self.dim.pitch_extent[1] mask_y_outside1 = bin_stat['cy'] < self.dim.pitch_extent[2] mask_y_outside2 = bin_stat['cy'] > self.dim.pitch_extent[3] mask_clip = mask_x_outside1 | mask_x_outside2 | mask_y_outside1 | mask_y_outside2 mask_clip = np.ravel(mask_clip) text = np.ravel(bin_stat['statistic'])[~mask_clip] cx = np.ravel(bin_stat['cx'])[~mask_clip] cy = np.ravel(bin_stat['cy'])[~mask_clip] for idx, text_str in enumerate(text): annotation = self.annotate(text_str, (cx[idx], cy[idx]), ax=ax, **kwargs) annotation_list.append(annotation) return annotation_list
def arrows(self, xstart, ystart, xend, yend, *args, ax=None, **kwargs): validate_ax(ax) quiver = arrows(xstart, ystart, xend, yend, *args, ax=ax, vertical=self.vertical, **kwargs) return quiver
def heatmap_positional(stats, ax=None, vertical=False, **kwargs): """ Plots several heatmaps for the different Juegos de posición areas. Parameters ---------- stats : A list of dictionaries. This should be calculated via bin_statistic_positional(). The dictionary keys are 'statistic' (the calculated statistic), 'x_grid' and 'y_grid (the bin's edges), and cx and cy (the bin centers). ax : matplotlib.axes.Axes, default None The axis to plot on. vertical : bool, default False If the orientation is vertical (True), then the code switches the x and y coordinates. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.pcolormesh. Returns ------- mesh : matplotlib.collections.QuadMesh Examples -------- >>> from mplsoccer import Pitch >>> import numpy as np >>> pitch = Pitch(line_zorder=2, pitch_color='black') >>> fig, ax = pitch.draw() >>> x = np.random.uniform(low=0, high=120, size=100) >>> y = np.random.uniform(low=0, high=80, size=100) >>> stats = pitch.bin_statistic_positional(x, y) >>> pitch.heatmap_positional(stats, edgecolors='black', cmap='hot', ax=ax) """ validate_ax(ax) # make vmin/vmax nan safe with np.nanmax/ np.nanmin vmax = kwargs.pop( 'vmax', np.nanmax([np.nanmax(stat['statistic']) for stat in stats])) vmin = kwargs.pop( 'vmin', np.nanmin([np.nanmin(stat['statistic']) for stat in stats])) mesh_list = [] for bin_stat in stats: mesh = heatmap(bin_stat, vmin=vmin, vmax=vmax, ax=ax, vertical=vertical, **kwargs) mesh_list.append(mesh) return mesh_list
def goal_angle(self, x, y, ax=None, goal='right', **kwargs): """ Plot a polygon with the angle to the goal using PatchCollection. See: https://matplotlib.org/stable/api/collections_api.html. Valid Collection keyword arguments: edgecolors, facecolors, linewidths, antialiaseds, transOffset, norm, cmap Parameters ---------- x, y: array-like or scalar. Commonly, these parameters are 1D arrays. These should be the coordinates on the pitch. goal: str default 'right'. The goal to plot, either 'left' or 'right'. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.collections.PathCollection. Returns ------- PatchCollection : matplotlib.collections.PatchCollection Examples -------- >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.goal_angle(100, 30, alpha=0.5, color='red', ax=ax) """ validate_ax(ax) valid_goal = ['left', 'right'] if goal not in valid_goal: raise TypeError( f'Invalid argument: goal should be in {valid_goal}') x = np.ravel(x) y = np.ravel(y) if x.size != y.size: raise ValueError("x and y must be the same size") if goal == 'right': goal_coordinates = self.goal_right else: goal_coordinates = self.goal_left verts = np.zeros((x.size, 3, 2)) verts[:, 0, 0] = x verts[:, 0, 1] = y verts[:, 1:, :] = np.expand_dims(goal_coordinates, 0) patch_collection = self.polygon(verts, ax=ax, **kwargs) return patch_collection
def polygon(self, verts, ax=None, **kwargs): """ Plot polygons using a PatchCollection. See: https://matplotlib.org/stable/api/collections_api.html. Automatically flips the x and y vertices if the pitch is vertical. Valid Collection keyword arguments: edgecolors, facecolors, linewidths, antialiaseds, transOffset, norm, cmap To use cmap use set_array as per this example: https://matplotlib.org/stable/gallery/shapes_and_collections/patch_collection.html. Parameters ---------- verts: verts is a sequence of (verts0, verts1, ...) where verts_i is a numpy array of shape (number of vertices, 2). ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.collections.PatchCollection. Returns ------- PatchCollection : matplotlib.collections.PatchCollection Examples -------- >>> from mplsoccer import Pitch >>> import numpy as np >>> pitch = Pitch(label=True, axis=True) >>> fig, ax = pitch.draw() >>> shape1 = np.array([[50, 2], [80, 30], [40, 30], [40, 20]]) >>> shape2 = np.array([[70, 70], [60, 50], [40, 40]]) >>> verts = [shape1, shape2] >>> pitch.polygon(verts, color='red', alpha=0.3, ax=ax) """ validate_ax(ax) patch_list = [] for vert in verts: vert = np.asarray(vert) vert = self._reverse_vertices_if_vertical(vert) polygon = patches.Polygon(vert, closed=True) patch_list.append(polygon) patch_collection = PatchCollection(patch_list, **kwargs) patch_collection = ax.add_collection(patch_collection) return patch_collection
def kdeplot(self, x, y, ax=None, **kwargs): """ Utility wrapper around seaborn.kdeplot, which automatically flips the x and y coordinates if the pitch is vertical and clips to the pitch boundaries. Parameters ---------- x, y : array-like or scalar. Commonly, these parameters are 1D arrays. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to seaborn.kdeplot. Returns ------- contour : matplotlib.contour.ContourSet Examples -------- >>> from mplsoccer import Pitch >>> import numpy as np >>> pitch = Pitch(line_zorder=2) >>> fig, ax = pitch.draw() >>> x = np.random.uniform(low=0, high=120, size=100) >>> y = np.random.uniform(low=0, high=80, size=100) >>> pitch.kdeplot(x, y, cmap='Reds', shade=True, levels=100, ax=ax) """ validate_ax(ax) x = np.ravel(x) y = np.ravel(y) if x.size != y.size: raise ValueError("x and y must be the same size") x, y = self._reverse_if_vertical(x, y) contour_plot = sns.kdeplot(x=x, y=y, ax=ax, clip=self.kde_clip, **kwargs) return contour_plot
def heatmap(stats, ax=None, vertical=False, **kwargs): """ Utility wrapper around matplotlib.axes.Axes.pcolormesh which automatically flips the x_grid and y_grid coordinates if the pitch is vertical. See: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.pcolormesh.html Parameters ---------- stats : dict. This should be calculated via bin_statistic(). The keys are 'statistic' (the calculated statistic), 'x_grid' and 'y_grid (the bin's edges), and cx and cy (the bin centers). ax : matplotlib.axes.Axes, default None The axis to plot on. vertical : bool, default False If the orientation is vertical (True), then the code switches the x and y coordinates. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.pcolormesh. Returns ------- mesh : matplotlib.collections.QuadMesh Examples -------- >>> from mplsoccer import Pitch >>> import numpy as np >>> pitch = Pitch(line_zorder=2, pitch_color='black') >>> fig, ax = pitch.draw() >>> x = np.random.uniform(low=0, high=120, size=100) >>> y = np.random.uniform(low=0, high=80, size=100) >>> stats = pitch.bin_statistic(x, y) >>> pitch.heatmap(stats, edgecolors='black', cmap='hot', ax=ax) """ validate_ax(ax) if vertical: mesh = ax.pcolormesh(stats['y_grid'], stats['x_grid'], stats['statistic'], **kwargs) else: mesh = ax.pcolormesh(stats['x_grid'], stats['y_grid'], stats['statistic'], **kwargs) return mesh
def annotate(self, text, xy, xytext=None, ax=None, **kwargs): """ Utility wrapper around ax.annotate which automatically flips the xy and xytext coordinates if the pitch is vertical. Annotate the point xy with text. See: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.annotate.html Parameters ---------- text : str The text of the annotation. xy : (float, float) The point (x, y) to annotate. xytext : (float, float), optional The position (x, y) to place the text at. If None, defaults to xy. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.annotate. Returns ------- annotation : matplotlib.text.Annotation Examples -------- >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.annotate(text='center', xytext=(50, 50), xy=(60, 40), ha='center', va='center', \ ax=ax, arrowprops=dict(facecolor='black')) """ validate_ax(ax) xy = self._reverse_annotate_if_vertical(xy) if xytext is not None: xytext = self._reverse_annotate_if_vertical(xytext) return ax.annotate(text, xy, xytext, **kwargs)
def flow(self, xstart, ystart, xend, yend, bins=(5, 4), arrow_type='same', arrow_length=5, color=None, ax=None, **kwargs): """ Create a flow map by binning the data into cells and calculating the average angles and distances. Parameters ---------- xstart, ystart, xend, yend: array-like or scalar. Commonly, these parameters are 1D arrays. These should be the start and end coordinates to calculate the angle between. bins : int or [int, int] or array_like or [array, array], optional The bin specification for binning the data to calculate the angles/ distances. * the number of bins for the two dimensions (nx = ny = bins), * the number of bins in each dimension (nx, ny = bins), * the bin edges for the two dimensions (x_edge = y_edge = bins), * the bin edges in each dimension (x_edge, y_edge = bins). If the bin edges are specified, the number of bins will be, (nx = len(x_edge)-1, ny = len(y_edge)-1). arrow_type : str, default 'same' The supported arrow types are: 'same', 'scale', and 'average'. 'same' makes the arrows the same size (arrow_length). 'scale' scales the arrow length by the average distance in the cell (up to a max of arrow_length). 'average' makes the arrow size the average distance in the cell. arrow_length : float, default 5 The arrow_length for the flow map. If the arrow_type='same', all the arrows will be arrow_length. If the arrow_type='scale', the arrows will be scaled by the average distance. If the arrow_type='average', the arrows_length is ignored This is automatically multipled by 100 if using a 'tracab' pitch (i.e. the default is 500). color : A matplotlib color, defaults to None. Defaults to None. In that case the marker color is determined by the cmap (default 'viridis'). and the counts of the starting positions in each bin. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.quiver. Returns ------- PolyCollection : matplotlib.quiver.Quiver Examples -------- >>> from mplsoccer import Pitch >>> from mplsoccer.statsbomb import read_event, EVENT_SLUG >>> df = read_event(f'{EVENT_SLUG}/7478.json', related_event_df=False, \ shot_freeze_frame_df=False, tactics_lineup_df=False)['event'] >>> team1, team2 = df.team_name.unique() >>> mask_team1 = (df.type_name == 'Pass') & (df.team_name == team1) >>> df = df[mask_team1].copy() >>> pitch = Pitch(line_zorder=2) >>> fig, ax = pitch.draw() >>> bs_heatmap = pitch.bin_statistic(df.x, df.y, statistic='count', bins=(6, 4)) >>> hm = pitch.heatmap(bs_heatmap, ax=ax, cmap='Blues') >>> fm = pitch.flow(df.x, df.y, df.end_x, df.end_y, color='black', arrow_type='same', \ arrow_length=6, bins=(6, 4), headwidth=2, headlength=2, \ headaxislength=2, ax=ax) """ validate_ax(ax) if self.dim.aspect != 1: standardized = True xstart, ystart = self.standardizer.transform(xstart, ystart) xend, yend = self.standardizer.transform(xend, yend) else: standardized = False # calculate the binned statistics angle, distance = self.calculate_angle_and_distance( xstart, ystart, xend, yend, standardized=standardized) bs_distance = self.bin_statistic(xstart, ystart, values=distance, statistic='mean', bins=bins, standardized=standardized) bs_angle = self.bin_statistic(xstart, ystart, values=angle, statistic=circmean, bins=bins, standardized=standardized) # calculate the arrow length if self.pitch_type == 'tracab': arrow_length = arrow_length * 100 if arrow_type == 'scale': new_d = (bs_distance['statistic'] * arrow_length / np.nan_to_num(bs_distance['statistic']).max(initial=None)) elif arrow_type == 'same': new_d = arrow_length elif arrow_type == 'average': new_d = bs_distance['statistic'] else: valid_arrows = ['scale', 'same', 'average'] raise TypeError( f'Invalid argument: arrow_type should be in {valid_arrows}') # calculate the end positions of the arrows endx = bs_angle['cx'] + (np.cos(bs_angle['statistic']) * new_d) if self.dim.invert_y and standardized is False: endy = bs_angle['cy'] - (np.sin(bs_angle['statistic']) * new_d ) # invert_y else: endy = bs_angle['cy'] + (np.sin(bs_angle['statistic']) * new_d) # get coordinates and convert back to the pitch coordinates if necessary cx, cy = bs_angle['cx'], bs_angle['cy'] if standardized: cx, cy = self.standardizer.transform(cx, cy, reverse=True) endx, endy = self.standardizer.transform(endx, endy, reverse=True) # plot arrows if color is None: bs_count = self.bin_statistic(xstart, ystart, statistic='count', bins=bins, standardized=standardized) flow = self.arrows(cx, cy, endx, endy, bs_count['statistic'], ax=ax, **kwargs) else: flow = self.arrows(cx, cy, endx, endy, color=color, ax=ax, **kwargs) return flow
def scatter(self, x, y, rotation_degrees=None, marker=None, ax=None, **kwargs): """ Utility wrapper around matplotlib.axes.Axes.scatter, which automatically flips the x and y coordinates if the pitch is vertical. You can optionally use a football marker with marker='football' and rotate markers with rotation_degrees. Parameters ---------- x, y : array-like or scalar. Commonly, these parameters are 1D arrays. rotation_degrees: array-like or scalar, default None. Rotates the marker in degrees, clockwise. 0 degrees is facing the direction of play. In a horizontal pitch, 0 degrees is this way →, in a vertical pitch, 0 degrees is this way ↑ marker: MarkerStyle, optional The marker style. marker can be either an instance of the class or the text shorthand for a particular marker. Defaults to None, in which case it takes the value of rcParams["scatter.marker"] (default: 'o') = 'o'. If marker='football' plots a football shape with the pentagons the color of the edgecolors and hexagons the color of the 'c' argument; 'linewidths' also sets the linewidth of the football marker. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.scatter. Returns ------- paths : matplotlib.collections.PathCollection or a tuple of (paths, paths) if marker='football' Examples -------- >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.scatter(30, 30, ax=ax) >>> from mplsoccer import Pitch >>> from mplsoccer import arrowhead_marker >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.scatter(30, 30, rotation_degrees=45, marker=arrowhead_marker, ax=ax) >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.scatter(30, 30, marker='football', ax=ax) """ validate_ax(ax) x = np.ma.ravel(x) y = np.ma.ravel(y) if x.size != y.size: raise ValueError("x and y must be the same size") x, y = self._reverse_if_vertical(x, y) if marker is None: marker = rcParams['scatter.marker'] if marker == 'football' and rotation_degrees is not None: raise NotImplementedError( "rotated football markers are not implemented.") if marker == 'football': scatter_plot = scatter_football(x, y, ax=ax, **kwargs) elif rotation_degrees is not None: scatter_plot = scatter_rotation(x, y, rotation_degrees, marker=marker, vertical=self.vertical, ax=ax, **kwargs) else: scatter_plot = ax.scatter(x, y, marker=marker, **kwargs) return scatter_plot
def hexbin(self, x, y, ax=None, **kwargs): """ Utility wrapper around matplotlib.axes.Axes.hexbin, which automatically flips the x and y coordinates if the pitch is vertical and clips to the pitch boundaries. Parameters ---------- x, y : array-like or scalar. Commonly, these parameters are 1D arrays. ax : matplotlib.axes.Axes, default None The axis to plot on. mincnt : int > 0, default: 1 If not None, only display cells with more than mincnt number of points in the cell. gridsize : int or (int, int), default: (17, 8) for Pitch/ (17, 17) for VerticalPitch If a single int, the number of hexagons in the x-direction. The number of hexagons in the y-direction is chosen such that the hexagons are approximately regular. Alternatively, if a tuple (nx, ny), the number of hexagons in the x-direction and the y-direction. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.hexbin. Returns ------- polycollection : matplotlib.collections.PolyCollection Examples -------- >>> from mplsoccer import Pitch >>> import numpy as np >>> pitch = Pitch(line_zorder=2) >>> fig, ax = pitch.draw() >>> x = np.random.uniform(low=0, high=120, size=100) >>> y = np.random.uniform(low=0, high=80, size=100) >>> pitch.hexbin(x, y, edgecolors='black', gridsize=(11, 5), cmap='Reds', ax=ax) """ validate_ax(ax) x = np.ravel(x) y = np.ravel(y) if x.size != y.size: raise ValueError("x and y must be the same size") # according to seaborn hexbin isn't nan safe so filter out nan mask = np.isnan(x) | np.isnan(y) x = x[~mask] y = y[~mask] x, y = self._reverse_if_vertical(x, y) mincnt = kwargs.pop('mincnt', 1) gridsize = kwargs.pop('gridsize', self.hexbin_gridsize) extent = kwargs.pop('extent', self.hex_extent) hexbin = ax.hexbin(x, y, mincnt=mincnt, gridsize=gridsize, extent=extent, **kwargs) rect = patches.Rectangle( (self.visible_pitch[0], self.visible_pitch[2]), self.visible_pitch[1] - self.visible_pitch[0], self.visible_pitch[3] - self.visible_pitch[2], fill=False) ax.add_patch(rect) hexbin.set_clip_path(rect) return hexbin
def lines(xstart, ystart, xend, yend, color=None, n_segments=100, comet=False, transparent=False, alpha_start=0.01, alpha_end=1, cmap=None, ax=None, vertical=False, reverse_cmap=False, **kwargs): """ Plots lines using matplotlib.collections.LineCollection. This is a fast way to plot multiple lines without loops. Also enables lines that increase in width or opacity by splitting the line into n_segments of increasing width or opacity as the line progresses. Parameters ---------- xstart, ystart, xend, yend: array-like or scalar. Commonly, these parameters are 1D arrays. These should be the start and end coordinates of the lines. color : A matplotlib color or sequence of colors, defaults to None. Defaults to None. In that case the marker color is determined by the value rcParams['lines.color'] n_segments : int, default 100 If comet=True or transparent=True this is used to split the line into n_segments of increasing width/opacity. comet : bool default False Whether to plot the lines increasing in width. transparent : bool, default False Whether to plot the lines increasing in opacity. linewidth or lw : array-like or scalar, default 5. Multiple linewidths not supported for the comet or transparent lines. alpha_start: float, default 0.01 The starting alpha value for transparent lines, between 0 (transparent) and 1 (opaque). If transparent = True the line will be drawn to linearly increase in opacity between alpha_start and alpha_end. alpha_end : float, default 1 The ending alpha value for transparent lines, between 0 (transparent) and 1 (opaque). If transparent = True the line will be drawn to linearly increase in opacity between alpha_start and alpha_end. cmap : str, default None A matplotlib cmap (colormap) name vertical : bool, default False If the orientation is vertical (True), then the code switches the x and y coordinates. reverse_cmap : bool, default False Whether to reverse the cmap colors. If the pitch is horizontal and the y-axis is inverted then set this to True. ax : matplotlib.axes.Axes, default None The axis to plot on. **kwargs : All other keyword arguments are passed on to matplotlib.collections.LineCollection. Returns ------- LineCollection : matplotlib.collections.LineCollection Examples -------- >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.lines(20, 20, 45, 80, comet=True, transparent=True, ax=ax) >>> from mplsoccer.linecollection import lines >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> lines([0.1, 0.4], [0.1, 0.5], [0.9, 0.4], [0.8, 0.8], ax=ax) """ validate_ax(ax) if not isinstance(comet, bool): raise TypeError( "Invalid argument: comet should be bool (True or False).") if not isinstance(transparent, bool): raise TypeError( "Invalid argument: transparent should be bool (True or False).") if alpha_start < 0 or alpha_start > 1: raise TypeError("alpha_start values should be within 0-1 range") if alpha_end < 0 or alpha_end > 1: raise TypeError("alpha_end values should be within 0-1 range") if alpha_start > alpha_end: msg = "Alpha start > alpha end. The line will increase in transparency nearer to the end" warnings.warn(msg) if 'colors' in kwargs.keys(): warnings.warn( "lines method takes 'color' as an argument, 'colors' in ignored") if color is not None and cmap is not None: raise ValueError("Only use one of color or cmap arguments not both.") if 'lw' in kwargs.keys() and 'linewidth' in kwargs.keys(): raise TypeError( "lines got multiple values for 'linewidth' argument (linewidth and lw)." ) # set linewidth if 'lw' in kwargs.keys(): lw = kwargs.pop('lw', 5) elif 'linewidth' in kwargs.keys(): lw = kwargs.pop('linewidth', 5) else: lw = 5 # to arrays xstart = np.ravel(xstart) ystart = np.ravel(ystart) xend = np.ravel(xend) yend = np.ravel(yend) lw = np.ravel(lw) if (comet or transparent) and (lw.size > 1): msg = "Multiple linewidths with a comet or transparent line is not implemented." raise NotImplementedError(msg) # set color if color is None and cmap is None: color = rcParams['lines.color'] if (comet or transparent) and (cmap is None) and ( to_rgba_array(color).shape[0] > 1): msg = "Multiple colors with a comet or transparent line is not implemented." raise NotImplementedError(msg) if xstart.size != ystart.size: raise ValueError("xstart and ystart must be the same size") if xstart.size != xend.size: raise ValueError("xstart and xend must be the same size") if ystart.size != yend.size: raise ValueError("ystart and yend must be the same size") if (lw.size > 1) and (lw.size != xstart.size): raise ValueError("lw and xstart must be the same size") if lw.size == 1: lw = lw[0] if vertical: ystart, xstart = xstart, ystart yend, xend = xend, yend # create linewidth if comet: lw = np.linspace(1, lw, n_segments) handler_first_lw = False else: handler_first_lw = True if (transparent is False) and (comet is False) and (cmap is None): multi_segment = False else: multi_segment = True if transparent: cmap = create_transparent_cmap(color, cmap, n_segments, alpha_start, alpha_end) if isinstance(cmap, str): cmap = get_cmap(cmap) if cmap is not None: handler_cmap = True line_collection = _lines_cmap(xstart, ystart, xend, yend, lw=lw, cmap=cmap, ax=ax, n_segments=n_segments, multi_segment=multi_segment, reverse_cmap=reverse_cmap, **kwargs) else: handler_cmap = False line_collection = _lines_no_cmap(xstart, ystart, xend, yend, lw=lw, color=color, ax=ax, n_segments=n_segments, multi_segment=multi_segment, **kwargs) line_collection_handler = HandlerLines(numpoints=n_segments, invert_y=reverse_cmap, first_lw=handler_first_lw, use_cmap=handler_cmap) Legend.update_default_handler_map( {line_collection: line_collection_handler}) return line_collection
def arrows(xstart, ystart, xend, yend, *args, ax=None, vertical=False, **kwargs): """ Utility wrapper around matplotlib.axes.Axes.quiver. Quiver uses locations and direction vectors usually. Here these are instead calculated automatically from the start and endpoints of the arrow. The function also automatically flips the x and y coordinates if the pitch is vertical. Plot a 2D field of arrows. See: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.quiver.html Parameters ---------- xstart, ystart, xend, yend: array-like or scalar. Commonly, these parameters are 1D arrays. These should be the start and end coordinates of the lines. C: 1D or 2D array-like, optional Numeric data that defines the arrow colors by colormapping via norm and cmap. This does not support explicit colors. If you want to set colors directly, use color instead. The size of C must match the number of arrow locations. ax : matplotlib.axes.Axes, default None The axis to plot on. vertical : bool, default False If the orientation is vertical (True), then the code switches the x and y coordinates. width : float, default 4 Arrow shaft width in points. headwidth : float, default 3 Head width as a multiple of the arrow shaft width. headlength : float, default 5 Head length as a multiple of the arrow shaft width. headaxislength : float, default: 4.5 Head length at the shaft intersection. If this is equal to the headlength then the arrow will be a triangular shape. If greater than the headlength then the arrow will be wedge shaped. If less than the headlength the arrow will be swept back. color : color or color sequence, optional Explicit color(s) for the arrows. If C has been set, color has no effect. linewidth or linewidths or lw : float or sequence of floats Edgewidth of arrow. edgecolor or ec or edgecolors : color or sequence of colors or 'face' alpha : float or None Transparency of arrows. **kwargs : All other keyword arguments are passed on to matplotlib.axes.Axes.quiver. Returns ------- PolyCollection : matplotlib.quiver.Quiver Examples -------- >>> from mplsoccer import Pitch >>> pitch = Pitch() >>> fig, ax = pitch.draw() >>> pitch.arrows(20, 20, 45, 80, ax=ax) >>> from mplsoccer.quiver import arrows >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> arrows([0.1, 0.4], [0.1, 0.5], [0.9, 0.4], [0.8, 0.8], ax=ax) >>> ax.set_xlim(0, 1) >>> ax.set_ylim(0, 1) """ validate_ax(ax) # set so plots in data units units = kwargs.pop('units', 'inches') scale_units = kwargs.pop('scale_units', 'xy') angles = kwargs.pop('angles', 'xy') scale = kwargs.pop('scale', 1) width = kwargs.pop('width', 4) # fixed a bug here. I changed the units to inches and divided by 72 # so the width is in points, i.e. 1/72th of an inch width = width / 72. xstart = np.ravel(xstart) ystart = np.ravel(ystart) xend = np.ravel(xend) yend = np.ravel(yend) if xstart.size != ystart.size: raise ValueError("xstart and ystart must be the same size") if xstart.size != xend.size: raise ValueError("xstart and xend must be the same size") if ystart.size != yend.size: raise ValueError("ystart and yend must be the same size") # vectors for direction u = xend - xstart v = yend - ystart if vertical: ystart, xstart = xstart, ystart v, u = u, v q = ax.quiver(xstart, ystart, u, v, *args, units=units, scale_units=scale_units, angles=angles, scale=scale, width=width, **kwargs) quiver_handler = HandlerQuiver() Legend.update_default_handler_map({q: quiver_handler}) return q