def plot_polar_heatmap(data, name, interp_factor=5., color_limits=False, hide_colorbar=False, vmin=None, vmax=None, log_scale=True, dpi=200, output_dir=None): """Plots the polar heatmap describing azimuth and latitude / elevation components. Plots the polar heatmap where each cell of the heatmap corresponds to the specific element of the array provided by `gather_polar_errors` function. Parameters ---------- data : 2D array Indicates the array containing the sum of angular errors within the specified angular ranges. It is usually provided by `gather_polar_errors` function. name : str Indicates the name of the output png file. interp_factor : float Indicates the interpolation factor of the heatmap. color_limits : boolean Specifies if the determined intensity limits should be returned. hide_colorbar : boolean Specifies if the colorbar should be hidden. vmin : float Indicates the minimum value of the colorbar. vmax : float Indicates the maximum value of the colorbar. log_scale : float Specifies if the heatmap sould be in the logarithmic scale. dpi : integer Indicates the DPI of the output image. output_dir : str Indicates the path to the output folder where the image will be stored. """ th0, th1 = 0., 180. r0, r1 = 0, 90 thlabel, rlabel = 'Azimuth', 'Elevation' tr_scale = Affine2D().scale(np.pi / 180., 1.) tr = tr_scale + PolarAxes.PolarTransform() lat_ticks = [(.0 * 90., '0$^{\circ}$'), (.33 * 90., '30$^{\circ}$'), (.66 * 90., '60$^{\circ}$'), (1. * 90., '90$^{\circ}$')] r_grid_locator = FixedLocator([v for v, s in lat_ticks]) r_grid_formatter = DictFormatter(dict(lat_ticks)) angle_ticks = [(0 * 180., '90$^{\circ}$'), (.25 * 180., '45$^{\circ}$'), (.5 * 180., '0$^{\circ}$'), (.75 * 180., '-45$^{\circ}$'), (1. * 180., '-90$^{\circ}$')] theta_grid_locator = FixedLocator([v for v, s in angle_ticks]) theta_tick_formatter = DictFormatter(dict(angle_ticks)) grid_helper = GridHelperCurveLinear(tr, extremes=(th0, th1, r0, r1), grid_locator1=theta_grid_locator, grid_locator2=r_grid_locator, tick_formatter1=theta_tick_formatter, tick_formatter2=r_grid_formatter) fig = plt.figure() ax = floating_axes.FloatingSubplot(fig, 111, grid_helper=grid_helper) fig.add_subplot(ax) ax.set_facecolor('white') ax.axis["bottom"].set_visible(False) ax.axis["top"].toggle(ticklabels=True, label=True) ax.axis["top"].set_axis_direction("bottom") ax.axis["top"].major_ticklabels.set_axis_direction("top") ax.axis["top"].label.set_axis_direction("top") ax.axis["left"].set_axis_direction("bottom") ax.axis["right"].set_axis_direction("top") ax.axis["top"].label.set_text(thlabel) ax.axis["left"].label.set_text(rlabel) aux_ax = ax.get_aux_axes(tr) aux_ax.patch = ax.patch ax.patch.zorder = 0.9 rad = np.linspace(0, 90, data.shape[1]) azm = np.linspace(0, 180, data.shape[0]) f = interpolate.interp2d(rad, azm, data, kind='linear', bounds_error=True, fill_value=0) new_rad = np.linspace(0, 90, 180 * interp_factor) new_azm = np.linspace(0, 180, 360 * interp_factor) new_data_angle_dist = f(new_rad, new_azm) new_r, new_th = np.meshgrid(new_rad, new_azm) new_data_angle_dist += 1. if log_scale: data_mesh = aux_ax.pcolormesh( new_th, new_r, new_data_angle_dist, cmap='jet', norm=colors.LogNorm( vmin=1. if vmin is None else vmin, vmax=new_data_angle_dist.max() if vmax is None else vmax)) else: data_mesh = aux_ax.pcolormesh(new_th, new_r, new_data_angle_dist, cmap='jet', vmin=vmin, vmax=vmax) cbar = plt.colorbar(data_mesh, orientation='vertical', shrink=.88, pad=.1, aspect=15) cbar.ax.set_ylabel('Absolute error, [deg.]') if hide_colorbar: cbar.remove() ax.grid(False) plt.show() if output_dir is not None: if not os.path.exists(output_dir): os.makedirs(output_dir) fig.savefig(os.path.join(output_dir, '{}_chart.png'.format(name)), transparent=False, bbox_inches='tight', pad_inches=0.1, dpi=dpi) if color_limits: return 1., new_data_angle_dist.max()
def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height))
def get_path_transform(self): if self.path_zoomable(): return self.collection.get_transform() - self.ax.transData else: # pixel coordinates start at top; we need to flip the path here return self.collection.get_transform() + Affine2D().scale(1., -1.)
def _get_affine_transform(self): return Affine2D() \ .scale(0.25) \ .translate(0.5, 0.5)
def __init__(self, parent_axes=None, parent_map=None, transform=None, coord_index=None, coord_type='scalar', coord_unit=None, coord_wrap=None, frame=None): # Keep a reference to the parent axes and the transform self.parent_axes = parent_axes self.parent_map = parent_map self.transform = transform self.coord_index = coord_index self.coord_unit = coord_unit self.frame = frame self.set_coord_type(coord_type, coord_wrap) # Initialize ticks self.dpi_transform = Affine2D() self.offset_transform = ScaledTranslation(0, 0, self.dpi_transform) self.ticks = Ticks(transform=parent_axes.transData + self.offset_transform) # Initialize tick labels self.ticklabels = TickLabels( self.frame, transform=None, # display coordinates figure=parent_axes.get_figure()) self.ticks.display_minor_ticks(False) self.minor_frequency = 5 # Initialize axis labels self.axislabels = AxisLabels( self.frame, transform=None, # display coordinates figure=parent_axes.get_figure()) # Initialize container for the grid lines self.grid_lines = [] # Initialize grid style. Take defaults from matplotlib.rcParams. # Based on matplotlib.axis.YTick._get_gridline. # # Matplotlib's gridlines use Line2D, but ours use PathPatch. # Patches take a slightly different format of linestyle argument. lines_to_patches_linestyle = { '-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted', 'none': 'none', 'None': 'none', ' ': 'none', '': 'none' } self.grid_lines_kwargs = { 'visible': False, 'facecolor': 'none', 'edgecolor': rcParams['grid.color'], 'linestyle': lines_to_patches_linestyle[rcParams['grid.linestyle']], 'linewidth': rcParams['grid.linewidth'], 'alpha': rcParams.get('grid.alpha', 1.0), 'transform': self.parent_axes.transData }
def ros_map(ax: plt.Axes, yaml_path: str, plot_mode: PlotMode, cmap: str = "Greys_r", mask_unknown_value: int = SETTINGS.ros_map_unknown_cell_value, alpha: float = SETTINGS.ros_map_alpha_value) -> None: """ Inserts an image of an 2D ROS map into the plot axis. See: http://wiki.ros.org/map_server#Map_format :param ax: 2D matplotlib axes :param plot_mode: a 2D PlotMode :param yaml_path: yaml file that contains the metadata of the map image :param cmap: color map used to map scalar data to colors :param mask_unknown_value: uint8 value that represents unknown cells. If specified, these cells will be masked out. If set to None or False, nothing will be masked. """ import yaml if isinstance(ax, Axes3D): raise PlotException("ros_map can't be drawn into a 3D axis") if plot_mode in {PlotMode.xz, PlotMode.yz, PlotMode.zx, PlotMode.zy}: # Image lies in xy / yx plane, nothing to see here. return x_idx, y_idx, _ = plot_mode_to_idx(plot_mode) with open(yaml_path) as f: metadata = yaml.safe_load(f) # Load map image, mask unknown cells if desired. image_path = metadata["image"] if not os.path.isabs(image_path): image_path = os.path.join(os.path.dirname(yaml_path), image_path) image = plt.imread(image_path) if mask_unknown_value: image = np.ma.masked_where(image == np.uint8(mask_unknown_value), image) # Squeeze extent to reflect metric coordinates. resolution = metadata["resolution"] n_rows, n_cols = image.shape[x_idx], image.shape[y_idx] extent = [0, n_cols * resolution, 0, n_rows * resolution] if plot_mode == PlotMode.yx: image = np.rot90(image) image = np.fliplr(image) ax_image = ax.imshow(image, origin="upper", cmap=cmap, extent=extent, zorder=1, alpha=alpha) # Transform map frame to plot axis origin. map_to_pixel_origin = Affine2D() map_to_pixel_origin.translate(metadata["origin"][x_idx], metadata["origin"][y_idx]) angle = metadata["origin"][2] if plot_mode == PlotMode.yx: # Rotation axis (z) points downwards. angle *= -1 map_to_pixel_origin.rotate(angle) ax_image.set_transform(map_to_pixel_origin + ax.transData) # Initially flipped axes are lost for mysterious reasons... if SETTINGS.plot_invert_xaxis: ax.invert_xaxis() if SETTINGS.plot_invert_yaxis: ax.invert_yaxis()
def get_axislabel_transform(self, axes): return Affine2D() # axes.transData
def test_cursor_data(): from matplotlib.backend_bases import MouseEvent fig, ax = plt.subplots() im = ax.imshow(np.arange(100).reshape(10, 10), origin='upper') x, y = 4, 4 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 44 # Now try for a point outside the image # Tests issue #4957 x, y = 10.1, 4 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) is None # Hmm, something is wrong here... I get 0, not None... # But, this works further down in the tests with extents flipped #x, y = 0.1, -0.1 #xdisp, ydisp = ax.transData.transform([x, y]) #event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) #z = im.get_cursor_data(event) #assert z is None, "Did not get None, got %d" % z ax.clear() # Now try with the extents flipped. im = ax.imshow(np.arange(100).reshape(10, 10), origin='lower') x, y = 4, 4 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 44 fig, ax = plt.subplots() im = ax.imshow(np.arange(100).reshape(10, 10), extent=[0, 0.5, 0, 0.5]) x, y = 0.25, 0.25 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 55 # Now try for a point outside the image # Tests issue #4957 x, y = 0.75, 0.25 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) is None x, y = 0.01, -0.01 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) is None # Now try with additional transform applied to the image artist trans = Affine2D().scale(2).rotate(0.5) im = ax.imshow(np.arange(100).reshape(10, 10), transform=trans + ax.transData) x, y = 3, 10 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 44
def test_axis_direction(): fig = plt.figure(figsize=(5, 5)) # PolarAxes.PolarTransform takes radian. However, we want our coordinate # system in degree tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() # polar projection, which involves cycle, and also has limits in # its coordinates, needs a special method to find the extremes # (min, max of the coordinate within the view). # 20, 20 : number of sampling points along x, y direction extreme_finder = angle_helper.ExtremeFinderCycle( 20, 20, lon_cycle=360, lat_cycle=None, lon_minmax=None, lat_minmax=(0, np.inf), ) grid_locator1 = angle_helper.LocatorDMS(12) tick_formatter1 = angle_helper.FormatterDMS() grid_helper = GridHelperCurveLinear(tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, tick_formatter1=tick_formatter1) ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) for axis in ax1.axis.values(): axis.set_visible(False) fig.add_subplot(ax1) ax1.axis["lat1"] = axis = grid_helper.new_floating_axis( 0, 130, axes=ax1, axis_direction="left") axis.label.set_text("Test") axis.label.set_visible(True) axis.get_helper()._extremes = 0.001, 10 ax1.axis["lat2"] = axis = grid_helper.new_floating_axis( 0, 50, axes=ax1, axis_direction="right") axis.label.set_text("Test") axis.label.set_visible(True) axis.get_helper()._extremes = 0.001, 10 ax1.axis["lon"] = axis = grid_helper.new_floating_axis( 1, 10, axes=ax1, axis_direction="bottom") axis.label.set_text("Test 2") axis.get_helper()._extremes = 50, 130 axis.major_ticklabels.set_axis_direction("top") axis.label.set_axis_direction("top") grid_helper.grid_finder.grid_locator1.den = 5 grid_helper.grid_finder.grid_locator2._nbins = 5 ax1.set_aspect(1.) ax1.set_xlim(-8, 8) ax1.set_ylim(-4, 12) ax1.grid(True)
def __init__(self, *args, **kwargs): Axes.__init__(self, *args, **kwargs) self.triangle_transform = Affine2D([[0.5, sqrt(3)/2],[0,1]]) self.cla()
def curvedEarthAxes(rect=111, fig=None, minground=0., maxground=2000, minalt=0, maxalt=500, Re=6371., nyticks=5, nxticks=4): """Create curved axes in ground-range and altitude Parameters ---------- rect : Optional[int] subplot spcification fig : Optional[pylab.figure object] (default to gcf) minground : Optional[float] maxground : Optional[int] maximum ground range [km] minalt : Optional[int] lowest altitude limit [km] maxalt : Optional[int] highest altitude limit [km] Re : Optional[float] Earth radius in kilometers nyticks : Optional[int] Number of y axis tick marks; default is 5 nxticks : Optional[int] Number of x axis tick marks; deafult is 4 Returns ------- ax : matplotlib.axes object containing formatting aax : matplotlib.axes object containing data Example ------- import numpy as np from utils import plotUtils ax, aax = plotUtils.curvedEarthAxes() th = np.linspace(0, ax.maxground/ax.Re, 50) r = np.linspace(ax.Re+ax.minalt, ax.Re+ax.maxalt, 20) Z = exp( -(r - 300 - ax.Re)**2 / 100**2 ) * np.cos(th[:, np.newaxis]/th.max()*4*np.pi) x, y = np.meshgrid(th, r) im = aax.pcolormesh(x, y, Z.T) ax.grid() written by Sebastien, 2013-04 """ from matplotlib.transforms import Affine2D, Transform import mpl_toolkits.axisartist.floating_axes as floating_axes from matplotlib.projections import polar from mpl_toolkits.axisartist.grid_finder import FixedLocator, DictFormatter import numpy as np from pylab import gcf ang = maxground / Re minang = minground / Re angran = ang - minang angle_ticks = [(0, "{:.0f}".format(minground))] while angle_ticks[-1][0] < angran: tang = angle_ticks[-1][0] + 1./nxticks*angran angle_ticks.append((tang, "{:.0f}".format((tang-minang)*Re))) grid_locator1 = FixedLocator([v for v, s in angle_ticks]) tick_formatter1 = DictFormatter(dict(angle_ticks)) altran = float(maxalt - minalt) alt_ticks = [(minalt+Re, "{:.0f}".format(minalt))] while alt_ticks[-1][0] < Re+maxalt: alt_ticks.append((altran / float(nyticks) + alt_ticks[-1][0], "{:.0f}".format(altran / float(nyticks) + alt_ticks[-1][0] - Re))) _ = alt_ticks.pop() grid_locator2 = FixedLocator([v for v, s in alt_ticks]) tick_formatter2 = DictFormatter(dict(alt_ticks)) tr_rotate = Affine2D().rotate(np.pi/2-ang/2) tr_shift = Affine2D().translate(0, Re) tr = polar.PolarTransform() + tr_rotate grid_helper = \ floating_axes.GridHelperCurveLinear(tr, extremes=(0, angran, Re+minalt, Re+maxalt), grid_locator1=grid_locator1, grid_locator2=grid_locator2, tick_formatter1=tick_formatter1, tick_formatter2=tick_formatter2,) if not fig: fig = gcf() ax1 = floating_axes.FloatingSubplot(fig, rect, grid_helper=grid_helper) # adjust axis ax1.axis["left"].label.set_text(r"Alt. [km]") ax1.axis["bottom"].label.set_text(r"Ground range [km]") ax1.invert_xaxis() ax1.minground = minground ax1.maxground = maxground ax1.minalt = minalt ax1.maxalt = maxalt ax1.Re = Re fig.add_subplot(ax1, transform=tr) # create a parasite axes whose transData in RA, cz aux_ax = ax1.get_aux_axes(tr) # for aux_ax to have a clip path as in ax aux_ax.patch = ax1.patch # but this has a side effect that the patch is drawn twice, and possibly # over some other artists. So, we decrease the zorder a bit to prevent this. ax1.patch.zorder=0.9 return ax1, aux_ax
def sgrid(): # From matplotlib demos: # https://matplotlib.org/gallery/axisartist/demo_curvelinear_grid.html # https://matplotlib.org/gallery/axisartist/demo_floating_axis.html # PolarAxes.PolarTransform takes radian. However, we want our coordinate # system in degree tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() # polar projection, which involves cycle, and also has limits in # its coordinates, needs a special method to find the extremes # (min, max of the coordinate within the view). # 20, 20 : number of sampling points along x, y direction sampling_points = 20 extreme_finder = ModifiedExtremeFinderCycle( sampling_points, sampling_points, lon_cycle=360, lat_cycle=None, lon_minmax=(90, 270), lat_minmax=(0, np.inf), ) grid_locator1 = angle_helper.LocatorDMS(15) tick_formatter1 = FormatterDMS() grid_helper = GridHelperCurveLinear(tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, tick_formatter1=tick_formatter1) fig = plt.figure() ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) # make ticklabels of right invisible, and top axis visible. visible = True ax.axis[:].major_ticklabels.set_visible(visible) ax.axis[:].major_ticks.set_visible(False) ax.axis[:].invert_ticklabel_direction() ax.axis["wnxneg"] = axis = ax.new_floating_axis(0, 180) axis.set_ticklabel_direction("-") axis.label.set_visible(False) ax.axis["wnxpos"] = axis = ax.new_floating_axis(0, 0) axis.label.set_visible(False) ax.axis["wnypos"] = axis = ax.new_floating_axis(0, 90) axis.label.set_visible(False) axis.set_axis_direction("left") ax.axis["wnyneg"] = axis = ax.new_floating_axis(0, 270) axis.label.set_visible(False) axis.set_axis_direction("left") axis.invert_ticklabel_direction() axis.set_ticklabel_direction("-") # let left axis shows ticklabels for 1st coordinate (angle) ax.axis["left"].get_helper().nth_coord_ticks = 0 ax.axis["right"].get_helper().nth_coord_ticks = 0 ax.axis["left"].get_helper().nth_coord_ticks = 0 ax.axis["bottom"].get_helper().nth_coord_ticks = 0 fig.add_subplot(ax) ### RECTANGULAR X Y AXES WITH SCALE #par2 = ax.twiny() #par2.axis["top"].toggle(all=False) #par2.axis["right"].toggle(all=False) #new_fixed_axis = par2.get_grid_helper().new_fixed_axis #par2.axis["left"] = new_fixed_axis(loc="left", # axes=par2, # offset=(0, 0)) #par2.axis["bottom"] = new_fixed_axis(loc="bottom", # axes=par2, # offset=(0, 0)) ### FINISH RECTANGULAR ax.grid(True, zorder=0, linestyle='dotted') _final_setup(ax) return ax, fig
def dpi_transform(self): return Affine2D().scale(1 / 72) + self.axes.figure.dpi_scale_trans
def curvelinear_test3(fig): """ polar projection, but in a rectangular box. """ global ax1, axis import numpy as np from . import angle_helper from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D from mpl_toolkits.axes_grid.parasite_axes import SubplotHost # PolarAxes.PolarTransform takes radian. However, we want our coordinate # system in degree tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() # polar projection, which involves cycle, and also has limits in # its coordinates, needs a special method to find the extremes # (min, max of the coordinate within the view). # 20, 20 : number of sampling points along x, y direction extreme_finder = angle_helper.ExtremeFinderCycle( 20, 20, lon_cycle=360, lat_cycle=None, lon_minmax=None, lat_minmax=(0, np.inf), ) grid_locator1 = angle_helper.LocatorDMS(12) # Find a grid values appropriate for the coordinate (degree, # minute, second). tick_formatter1 = angle_helper.FormatterDMS() # And also uses an appropriate formatter. Note that,the # acceptable Locator and Formatter class is a bit different than # that of mpl's, and you cannot directly use mpl's Locator and # Formatter here (but may be possible in the future). grid_helper = GridHelperCurveLinear(tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, tick_formatter1=tick_formatter1) ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) for axis in list(six.itervalues(ax1.axis)): axis.set_visible(False) fig.add_subplot(ax1) grid_helper = ax1.get_grid_helper() ax1.axis["lat1"] = axis = grid_helper.new_floating_axis( 0, 130, axes=ax1, axis_direction="left") axis.label.set_text("Test") axis.label.set_visible(True) axis.get_helper()._extremes = 0.001, 10 grid_helper = ax1.get_grid_helper() ax1.axis["lat2"] = axis = grid_helper.new_floating_axis( 0, 50, axes=ax1, axis_direction="right") axis.label.set_text("Test") axis.label.set_visible(True) axis.get_helper()._extremes = 0.001, 10 ax1.axis["lon"] = axis = grid_helper.new_floating_axis( 1, 10, axes=ax1, axis_direction="bottom") axis.label.set_text("Test 2") axis.get_helper()._extremes = 50, 130 axis.major_ticklabels.set_axis_direction("top") axis.label.set_axis_direction("top") grid_helper.grid_finder.grid_locator1.den = 5 grid_helper.grid_finder.grid_locator2._nbins = 5 # # A parasite axes with given transform # ax2 = ParasiteAxesAuxTrans(ax1, tr, "equal") # # note that ax2.transData == tr + ax1.transData # # Anthing you draw in ax2 will match the ticks and grids of ax1. # ax1.parasites.append(ax2) # intp = cbook.simple_linear_interpolation # ax2.plot(intp(np.array([0, 30]), 50), # intp(np.array([10., 10.]), 50)) ax1.set_aspect(1.) ax1.set_xlim(-5, 12) ax1.set_ylim(-5, 10) ax1.grid(True)
def draw_image(self, gc, x, y, im, transform=None): # docstring inherited h, w = im.shape[:2] if w == 0 or h == 0: return attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) oid = gc.get_gid() url = gc.get_url() if url is not None: self.writer.start('a', attrib={'xlink:href': url}) if mpl.rcParams['svg.image_inline']: buf = BytesIO() Image.fromarray(im).save(buf, format="png") oid = oid or self._make_id('image', buf.getvalue()) attrib['xlink:href'] = ( "data:image/png;base64,\n" + base64.b64encode(buf.getvalue()).decode('ascii')) else: if self.basename is None: raise ValueError("Cannot save image data to filesystem when " "writing SVG to an in-memory buffer") filename = '{}.image{}.png'.format( self.basename, next(self._image_counter)) _log.info('Writing image file for inclusion: %s', filename) Image.fromarray(im).save(filename) oid = oid or 'Im_' + self._make_id('image', filename) attrib['xlink:href'] = filename attrib['id'] = oid if transform is None: w = 72.0 * w / self.image_dpi h = 72.0 * h / self.image_dpi self.writer.element( 'image', transform=generate_transform([ ('scale', (1, -1)), ('translate', (0, -h))]), x=short_float_fmt(x), y=short_float_fmt(-(self.height - y - h)), width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) else: alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = short_float_fmt(alpha) flipped = ( Affine2D().scale(1.0 / w, 1.0 / h) + transform + Affine2D() .translate(x, y) .scale(1.0, -1.0) .translate(0.0, self.height)) attrib['transform'] = generate_transform( [('matrix', flipped.frozen())]) attrib['style'] = ( 'image-rendering:crisp-edges;' 'image-rendering:pixelated') self.writer.element( 'image', width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) if url is not None: self.writer.end('a') if clipid is not None: self.writer.end('g')
def setup_axes(fig, rect, theta, radius, quad): # quad controls the quadrant of the plot and controls the orientation # of the plot where quad=1 is upper left, 2 is upper right, 3 is # lower left, 4 is lower right if quad == 1: tr_rotate = Affine2D().translate(np.pi / 2.0, 0) elif quad == 2: tr_rotate = Affine2D().translate(0, 0) elif quad == 3: tr_rotate = Affine2D().translate(np.pi, 0) else: tr_rotate = Affine2D().translate(3.0 * np.pi / 2.0, 0) tr_scale = Affine2D().scale(np.pi / 180., 1.) # PolarAxes.PolarTransform takes radian. However, we want our coordinate # system in degree tr = tr_scale + tr_rotate + PolarAxes.PolarTransform() # Find grid values appropriate for the coordinate (degree). # The argument is an approximate number of grids. grid_locator1 = angle_helper.LocatorD(2) # And also use an appropriate formatter: tick_formatter1 = angle_helper.FormatterDMS() # set up number of ticks for the r-axis grid_locator2 = MaxNLocator(5) grid_locator1 = MaxNLocator(6) # the extremes are passed to the function thetaMin = 0 thetaMax = 90 grid_helper = floating_axes.GridHelperCurveLinear( tr, extremes=(thetaMin, thetaMax, radius[0], radius[1]), grid_locator1=grid_locator1, grid_locator2=grid_locator2, tick_formatter1=tick_formatter1, tick_formatter2=None, ) ax1 = floating_axes.FloatingSubplot(fig, rect, grid_helper=grid_helper) fig.add_subplot(ax1) print ax1.get_xlim() print ax1.get_ylim() # adjust axis # the axis artist lets you call axis with # "bottom", "top", "left", "right" ax1.axis["left"].set_axis_direction("bottom") ax1.axis["right"].set_axis_direction("top") ax1.axis["bottom"].set_visible(False) ax1.axis["top"].set_axis_direction("bottom") ax1.axis["top"].toggle(ticklabels=True, label=True) ax1.axis["top"].major_ticklabels.set_axis_direction("top") ax1.axis["top"].label.set_axis_direction("top") ax1.axis["top"].label.set_text(ur"$\Phi$") # Set the axis labels based on the quadrant if quad == 1: # Set visibilities ax1.axis["right"].set_visible(True) ax1.axis["left"].set_visible(True) ax1.axis["right"].toggle(ticklabels=True, label=True) ax1.axis["left"].toggle(ticklabels=False, label=False) # Tick Labels #ax1.axis["right"].major_ticks.set_axis_direction('right') ax1.axis["right"].major_ticklabels.set_axis_direction('bottom') #ax1.axis["right"].major_ticklabels.set_pad(-20) #ax1.axis["right"].set_axis_direction('top') #ax1.axis["right"].set_ticklabel_direction('+') # Axis labels ax1.axis["right"].label.set_rotation(0) ax1.axis["right"].label.set_text("$D$ (kpc)") ax1.axis["right"].label.set_pad(20) elif quad == 2: ax1.axis["left"].set_visible(True) ax1.axis["left"].toggle(ticklabels=True, label=True) ax1.axis["left"].major_ticklabels.set_axis_direction("bottom") ax1.axis["left"].set_axis_direction("bottom") ax1.axis["left"].label.set_text("$D$ (kpc)") elif quad == 3: # Set visibilities ax1.axis["right"].set_visible(True) ax1.axis["left"].set_visible(True) ax1.axis["right"].toggle(ticklabels=False, label=False) ax1.axis["left"].toggle(ticklabels=True, label=True) # Tick Labels #ax1.axis["right"].major_ticks.set_axis_direction('right') ax1.axis["left"].major_ticklabels.set_axis_direction('top') #ax1.axis["right"].major_ticklabels.set_pad(-20) #ax1.axis["right"].set_axis_direction('top') #ax1.axis["right"].set_ticklabel_direction('+') # Axis labels ax1.axis["left"].label.set_rotation(180) ax1.axis["left"].label.set_text("$D$ (kpc)") ax1.axis["left"].label.set_pad(20) else: # Set visibilities ax1.axis["right"].set_visible(True) ax1.axis["left"].set_visible(True) ax1.axis["right"].toggle(ticklabels=True, label=True) ax1.axis["left"].toggle(ticklabels=False, label=False) # Tick Labels #ax1.axis["right"].major_ticks.set_axis_direction('right') ax1.axis["right"].major_ticklabels.set_axis_direction('top') #ax1.axis["right"].major_ticklabels.set_pad(-20) #ax1.axis["right"].set_axis_direction('top') #ax1.axis["right"].set_ticklabel_direction('+') # Axis labels ax1.axis["right"].label.set_rotation(180) ax1.axis["right"].label.set_text("$D$ (kpc)") ax1.axis["right"].label.set_pad(0) ax1.grid(True) # create a parasite axes aux_ax = ax1.get_aux_axes(tr) aux_ax.patch = ax1.patch # for aux_ax to have a clip path as in ax ax1.patch.zorder = 0.9 # but this has a side effect that the patch is # drawn twice, and possibly over some other # artists. So, we decrease the zorder a bit to # prevent this. return ax1, aux_ax
def _setup_axes1(fig, angle, left, right, bottom, up): """ Stacks a rotated axes over the main one. Required arguments: ---------- *fig*: The figure that the axes are in. *angle*: A numeric limited in [-15, +15]. The angle of rotation of the canvas. *left*: A numeric. Leftmost point of the printable area. *right*: A numeric. Rightmost point of the printable area. *bottom*: A numeric. Lowest point of the printable area. *up*: A numeric. Highest point of the printable area. Return: ----------- Two axes: a background one (ax1) and a rotated one (ax). The plot will be displayed on the rotated ax. """ import mpl_toolkits.axisartist.floating_axes as floating_axes from mpl_toolkits.axisartist.grid_finder import MaxNLocator from matplotlib.transforms import Affine2D # Define height to width ratio vert = up - bottom hor = right - left ratio = vert / hor # Create rotated and scaled canvas tr = Affine2D().scale(4*ratio, 4).rotate_deg(angle) # Rotated canvas is in the center tile of a 3x3 grid, limited by extremes grid_helper = floating_axes.GridHelperCurveLinear( tr, extremes=(left, right, bottom, up), grid_locator1=MaxNLocator(nbins=4), grid_locator2=MaxNLocator(nbins=4)) ax = floating_axes.FloatingSubplot(fig, 111, grid_helper=grid_helper) fig.add_subplot(ax) ax1 = ax.get_aux_axes(tr) for axisLoc in ['top','left','right', 'bottom']: ax.axis[axisLoc].set_visible(False) ax1.axis[axisLoc].set_visible(False) return ax1, ax
i = 0 code_map = { 'M': (Path.MOVETO, 1), 'C': (Path.CURVE4, 3), 'L': (Path.LINETO, 1) } while i < len(parts): code = parts[i] path_code, npoints = code_map[code] codes.extend([path_code] * npoints) vertices.extend([[float(x) for x in y.split(',')] for y in parts[i + 1:i + npoints + 1]]) i += npoints + 1 vertices = np.array(vertices, float) vertices[:, 1] -= 160 dolphin_path = Path(vertices, codes) dolphin_patch = PathPatch(dolphin_path, facecolor=(0.6, 0.6, 0.6), edgecolor=(0.0, 0.0, 0.0)) ax.add_patch(dolphin_patch) vertices = Affine2D().rotate_deg(60).transform(vertices) dolphin_path2 = Path(vertices, codes) dolphin_patch2 = PathPatch(dolphin_path2, facecolor=(0.5, 0.5, 0.5), edgecolor=(0.0, 0.0, 0.0)) ax.add_patch(dolphin_patch2) plt.show()
def adjust_bbox(fig, bbox_inches, fixed_dpi=None): """ Temporarily adjust the figure so that only the specified area (bbox_inches) is saved. It modifies fig.bbox, fig.bbox_inches, fig.transFigure._boxout, and fig.patch. While the figure size changes, the scale of the original figure is conserved. A function which restores the original values are returned. """ def no_op_apply_aspect(position=None): return origBbox = fig.bbox origBboxInches = fig.bbox_inches orig_tight_layout = fig.get_tight_layout() _boxout = fig.transFigure._boxout fig.set_tight_layout(False) old_aspect = [] locator_list = [] sentinel = object() for ax in fig.axes: pos = ax.get_position(original=False).frozen() locator_list.append(ax.get_axes_locator()) def _l(a, r, pos=pos): return pos ax.set_axes_locator(_l) # override the method that enforces the aspect ratio # on the Axes if 'apply_aspect' in ax.__dict__: old_aspect.append(ax.apply_aspect) else: old_aspect.append(sentinel) ax.apply_aspect = no_op_apply_aspect def restore_bbox(): for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect): ax.set_axes_locator(loc) if aspect is sentinel: # delete our no-op function which un-hides the # original method del ax.apply_aspect else: ax.apply_aspect = aspect fig.bbox = origBbox fig.bbox_inches = origBboxInches fig.set_tight_layout(orig_tight_layout) fig.transFigure._boxout = _boxout fig.transFigure.invalidate() fig.patch.set_bounds(0, 0, 1, 1) if fixed_dpi is not None: tr = Affine2D().scale(fixed_dpi) dpi_scale = fixed_dpi / fig.dpi else: tr = Affine2D().scale(fig.dpi) dpi_scale = 1. _bbox = TransformedBbox(bbox_inches, tr) fig.bbox_inches = Bbox.from_bounds(0, 0, bbox_inches.width, bbox_inches.height) x0, y0 = _bbox.x0, _bbox.y0 w1, h1 = fig.bbox.width * dpi_scale, fig.bbox.height * dpi_scale fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) fig.transFigure.invalidate() fig.bbox = TransformedBbox(fig.bbox_inches, tr) fig.patch.set_bounds(x0 / w1, y0 / h1, fig.bbox.width / w1, fig.bbox.height / h1) return restore_bbox
df = df[["Player", "Pos", "90s", "Carries_1/3", "1/3"]] mf_positions = ['MF'] min_90s = 8 df = df[(df["90s"] > min_90s) & (df["Pos"].isin(mf_positions))].reset_index(drop=True) df[["Carries_1/3", "1/3"]] = df[["Carries_1/3", "1/3"]].div(df["90s"], axis=0) xs = StandardScaler().fit_transform(df["Carries_1/3"].values.reshape(-1, 1)) ys = StandardScaler().fit_transform(df["1/3"].values.reshape(-1, 1)) with plt.style.context("custom_viz_dark"): fig = plt.figure(figsize=(8, 8)) plot_extents = -2.4, 5.6, -2.4, 5.6 transform = Affine2D().rotate_deg(45) helper = floating_axes.GridHelperCurveLinear(transform, plot_extents) ax = floating_axes.FloatingSubplot(fig, 111, grid_helper=helper) ax.grid(alpha=0.5, linestyle="-.") fig.add_subplot(ax) ax.scatter(xs, ys, ec='k', alpha=.5, s=50, marker="h") ax.set_aspect(1) ###highlight top percentile players player_names = list( set( df.sort_values("Carries_1/3")["Player"].tail(7).tolist() + df.sort_values("1/3")["Player"].tail(7).tolist())) sel_df = df.query("Player == @player_names")
def add(self, patchlabel='', flows=None, orientations=None, labels='', trunklength=1.0, pathlengths=0.25, prior=None, connect=(0, 0), rotation=0, **kwargs): """ Add a simple Sankey diagram with flows at the same hierarchical level. Parameters ---------- patchlabel : str Label to be placed at the center of the diagram. Note that *label* (not *patchlabel*) can be passed as keyword argument to create an entry in the legend. flows : list of float Array of flow values. By convention, inputs are positive and outputs are negative. Flows are placed along the top of the diagram from the inside out in order of their index within *flows*. They are placed along the sides of the diagram from the top down and along the bottom from the outside in. If the sum of the inputs and outputs is nonzero, the discrepancy will appear as a cubic Bezier curve along the top and bottom edges of the trunk. orientations : list of {-1, 0, 1} List of orientations of the flows (or a single orientation to be used for all flows). Valid values are 0 (inputs from the left, outputs to the right), 1 (from and to the top) or -1 (from and to the bottom). labels : list of (str or None) List of labels for the flows (or a single label to be used for all flows). Each label may be *None* (no label), or a labeling string. If an entry is a (possibly empty) string, then the quantity for the corresponding flow will be shown below the string. However, if the *unit* of the main diagram is None, then quantities are never shown, regardless of the value of this argument. trunklength : float Length between the bases of the input and output groups (in data-space units). pathlengths : list of float List of lengths of the vertical arrows before break-in or after break-away. If a single value is given, then it will be applied to the first (inside) paths on the top and bottom, and the length of all other arrows will be justified accordingly. The *pathlengths* are not applied to the horizontal inputs and outputs. prior : int Index of the prior diagram to which this diagram should be connected. connect : (int, int) A (prior, this) tuple indexing the flow of the prior diagram and the flow of this diagram which should be connected. If this is the first diagram or *prior* is *None*, *connect* will be ignored. rotation : float Angle of rotation of the diagram in degrees. The interpretation of the *orientations* argument will be rotated accordingly (e.g., if *rotation* == 90, an *orientations* entry of 1 means to/from the left). *rotation* is ignored if this diagram is connected to an existing one (using *prior* and *connect*). Returns ------- Sankey The current `.Sankey` instance. Other Parameters ---------------- **kwargs Additional keyword arguments set `matplotlib.patches.PathPatch` properties, listed below. For example, one may want to use ``fill=False`` or ``label="A legend entry"``. %(Patch)s See Also -------- Sankey.finish """ # Check and preprocess the arguments. if flows is None: flows = np.array([1.0, -1.0]) else: flows = np.array(flows) n = flows.shape[0] # Number of flows if rotation is None: rotation = 0 else: # In the code below, angles are expressed in deg/90. rotation /= 90.0 if orientations is None: orientations = 0 try: orientations = np.broadcast_to(orientations, n) except ValueError: raise ValueError( f"The shapes of 'flows' {np.shape(flows)} and 'orientations' " f"{np.shape(orientations)} are incompatible") from None try: labels = np.broadcast_to(labels, n) except ValueError: raise ValueError( f"The shapes of 'flows' {np.shape(flows)} and 'labels' " f"{np.shape(labels)} are incompatible") from None if trunklength < 0: raise ValueError( "'trunklength' is negative, which is not allowed because it " "would cause poor layout") if abs(np.sum(flows)) > self.tolerance: _log.info( "The sum of the flows is nonzero (%f; patchlabel=%r); " "is the system not at steady state?", np.sum(flows), patchlabel) scaled_flows = self.scale * flows gain = sum(max(flow, 0) for flow in scaled_flows) loss = sum(min(flow, 0) for flow in scaled_flows) if prior is not None: if prior < 0: raise ValueError("The index of the prior diagram is negative") if min(connect) < 0: raise ValueError( "At least one of the connection indices is negative") if prior >= len(self.diagrams): raise ValueError( f"The index of the prior diagram is {prior}, but there " f"are only {len(self.diagrams)} other diagrams") if connect[0] >= len(self.diagrams[prior].flows): raise ValueError( "The connection index to the source diagram is {}, but " "that diagram has only {} flows".format( connect[0], len(self.diagrams[prior].flows))) if connect[1] >= n: raise ValueError( f"The connection index to this diagram is {connect[1]}, " f"but this diagram has only {n} flows") if self.diagrams[prior].angles[connect[0]] is None: raise ValueError( f"The connection cannot be made, which may occur if the " f"magnitude of flow {connect[0]} of diagram {prior} is " f"less than the specified tolerance") flow_error = (self.diagrams[prior].flows[connect[0]] + flows[connect[1]]) if abs(flow_error) >= self.tolerance: raise ValueError( f"The scaled sum of the connected flows is {flow_error}, " f"which is not within the tolerance ({self.tolerance})") # Determine if the flows are inputs. are_inputs = [None] * n for i, flow in enumerate(flows): if flow >= self.tolerance: are_inputs[i] = True elif flow <= -self.tolerance: are_inputs[i] = False else: _log.info( "The magnitude of flow %d (%f) is below the tolerance " "(%f).\nIt will not be shown, and it cannot be used in a " "connection.", i, flow, self.tolerance) # Determine the angles of the arrows (before rotation). angles = [None] * n for i, (orient, is_input) in enumerate(zip(orientations, are_inputs)): if orient == 1: if is_input: angles[i] = DOWN elif not is_input: # Be specific since is_input can be None. angles[i] = UP elif orient == 0: if is_input is not None: angles[i] = RIGHT else: if orient != -1: raise ValueError( f"The value of orientations[{i}] is {orient}, " f"but it must be -1, 0, or 1") if is_input: angles[i] = UP elif not is_input: angles[i] = DOWN # Justify the lengths of the paths. if np.iterable(pathlengths): if len(pathlengths) != n: raise ValueError( f"The lengths of 'flows' ({n}) and 'pathlengths' " f"({len(pathlengths)}) are incompatible") else: # Make pathlengths into a list. urlength = pathlengths ullength = pathlengths lrlength = pathlengths lllength = pathlengths d = dict(RIGHT=pathlengths) pathlengths = [d.get(angle, 0) for angle in angles] # Determine the lengths of the top-side arrows # from the middle outwards. for i, (angle, is_input, flow) in enumerate(zip(angles, are_inputs, scaled_flows)): if angle == DOWN and is_input: pathlengths[i] = ullength ullength += flow elif angle == UP and not is_input: pathlengths[i] = urlength urlength -= flow # Flow is negative for outputs. # Determine the lengths of the bottom-side arrows # from the middle outwards. for i, (angle, is_input, flow) in enumerate( reversed(list(zip(angles, are_inputs, scaled_flows)))): if angle == UP and is_input: pathlengths[n - i - 1] = lllength lllength += flow elif angle == DOWN and not is_input: pathlengths[n - i - 1] = lrlength lrlength -= flow # Determine the lengths of the left-side arrows # from the bottom upwards. has_left_input = False for i, (angle, is_input, spec) in enumerate( reversed( list( zip(angles, are_inputs, zip(scaled_flows, pathlengths))))): if angle == RIGHT: if is_input: if has_left_input: pathlengths[n - i - 1] = 0 else: has_left_input = True # Determine the lengths of the right-side arrows # from the top downwards. has_right_output = False for i, (angle, is_input, spec) in enumerate( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == RIGHT: if not is_input: if has_right_output: pathlengths[i] = 0 else: has_right_output = True # Begin the subpaths, and smooth the transition if the sum of the flows # is nonzero. urpath = [ ( Path.MOVETO, [ (self.gap - trunklength / 2.0), # Upper right gain / 2.0 ]), (Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0, gain / 2.0]), (Path.CURVE4, [(self.gap - trunklength / 2.0) / 8.0, gain / 2.0]), (Path.CURVE4, [(trunklength / 2.0 - self.gap) / 8.0, -loss / 2.0]), (Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0, -loss / 2.0]), (Path.LINETO, [(trunklength / 2.0 - self.gap), -loss / 2.0]) ] llpath = [ ( Path.LINETO, [ (trunklength / 2.0 - self.gap), # Lower left loss / 2.0 ]), (Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0, loss / 2.0]), (Path.CURVE4, [(trunklength / 2.0 - self.gap) / 8.0, loss / 2.0]), (Path.CURVE4, [(self.gap - trunklength / 2.0) / 8.0, -gain / 2.0]), (Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0, -gain / 2.0]), (Path.LINETO, [(self.gap - trunklength / 2.0), -gain / 2.0]) ] lrpath = [( Path.LINETO, [ (trunklength / 2.0 - self.gap), # Lower right loss / 2.0 ])] ulpath = [( Path.LINETO, [ self.gap - trunklength / 2.0, # Upper left gain / 2.0 ])] # Add the subpaths and assign the locations of the tips and labels. tips = np.zeros((n, 2)) label_locations = np.zeros((n, 2)) # Add the top-side inputs and outputs from the middle outwards. for i, (angle, is_input, spec) in enumerate( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == DOWN and is_input: tips[i, :], label_locations[i, :] = self._add_input( ulpath, angle, *spec) elif angle == UP and not is_input: tips[i, :], label_locations[i, :] = self._add_output( urpath, angle, *spec) # Add the bottom-side inputs and outputs from the middle outwards. for i, (angle, is_input, spec) in enumerate( reversed( list( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))))): if angle == UP and is_input: tip, label_location = self._add_input(llpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location elif angle == DOWN and not is_input: tip, label_location = self._add_output(lrpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location # Add the left-side inputs from the bottom upwards. has_left_input = False for i, (angle, is_input, spec) in enumerate( reversed( list( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))))): if angle == RIGHT and is_input: if not has_left_input: # Make sure the lower path extends # at least as far as the upper one. if llpath[-1][1][0] > ulpath[-1][1][0]: llpath.append( (Path.LINETO, [ulpath[-1][1][0], llpath[-1][1][1]])) has_left_input = True tip, label_location = self._add_input(llpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location # Add the right-side outputs from the top downwards. has_right_output = False for i, (angle, is_input, spec) in enumerate( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == RIGHT and not is_input: if not has_right_output: # Make sure the upper path extends # at least as far as the lower one. if urpath[-1][1][0] < lrpath[-1][1][0]: urpath.append( (Path.LINETO, [lrpath[-1][1][0], urpath[-1][1][1]])) has_right_output = True tips[i, :], label_locations[i, :] = self._add_output( urpath, angle, *spec) # Trim any hanging vertices. if not has_left_input: ulpath.pop() llpath.pop() if not has_right_output: lrpath.pop() urpath.pop() # Concatenate the subpaths in the correct order (clockwise from top). path = (urpath + self._revert(lrpath) + llpath + self._revert(ulpath) + [(Path.CLOSEPOLY, urpath[0][1])]) # Create a patch with the Sankey outline. codes, vertices = zip(*path) vertices = np.array(vertices) def _get_angle(a, r): if a is None: return None else: return a + r if prior is None: if rotation != 0: # By default, none of this is needed. angles = [_get_angle(angle, rotation) for angle in angles] rotate = Affine2D().rotate_deg(rotation * 90).transform_affine tips = rotate(tips) label_locations = rotate(label_locations) vertices = rotate(vertices) text = self.ax.text(0, 0, s=patchlabel, ha='center', va='center') else: rotation = (self.diagrams[prior].angles[connect[0]] - angles[connect[1]]) angles = [_get_angle(angle, rotation) for angle in angles] rotate = Affine2D().rotate_deg(rotation * 90).transform_affine tips = rotate(tips) offset = self.diagrams[prior].tips[connect[0]] - tips[connect[1]] translate = Affine2D().translate(*offset).transform_affine tips = translate(tips) label_locations = translate(rotate(label_locations)) vertices = translate(rotate(vertices)) kwds = dict(s=patchlabel, ha='center', va='center') text = self.ax.text(*offset, **kwds) if rcParams['_internal.classic_mode']: fc = kwargs.pop('fc', kwargs.pop('facecolor', '#bfd1d4')) lw = kwargs.pop('lw', kwargs.pop('linewidth', 0.5)) else: fc = kwargs.pop('fc', kwargs.pop('facecolor', None)) lw = kwargs.pop('lw', kwargs.pop('linewidth', None)) if fc is None: fc = next(self.ax._get_patches_for_fill.prop_cycler)['color'] patch = PathPatch(Path(vertices, codes), fc=fc, lw=lw, **kwargs) self.ax.add_patch(patch) # Add the path labels. texts = [] for number, angle, label, location in zip(flows, angles, labels, label_locations): if label is None or angle is None: label = '' elif self.unit is not None: quantity = self.format % abs(number) + self.unit if label != '': label += "\n" label += quantity texts.append( self.ax.text(x=location[0], y=location[1], s=label, ha='center', va='center')) # Text objects are placed even they are empty (as long as the magnitude # of the corresponding flow is larger than the tolerance) in case the # user wants to provide labels later. # Expand the size of the diagram if necessary. self.extent = (min(np.min(vertices[:, 0]), np.min(label_locations[:, 0]), self.extent[0]), max(np.max(vertices[:, 0]), np.max(label_locations[:, 0]), self.extent[1]), min(np.min(vertices[:, 1]), np.min(label_locations[:, 1]), self.extent[2]), max(np.max(vertices[:, 1]), np.max(label_locations[:, 1]), self.extent[3])) # Include both vertices _and_ label locations in the extents; there are # where either could determine the margins (e.g., arrow shoulders). # Add this diagram as a subdiagram. self.diagrams.append( SimpleNamespace(patch=patch, flows=flows, angles=angles, tips=tips, text=text, texts=texts)) # Allow a daisy-chained call structure (see docstring for the class). return self
def make_flip_transform(self, transform): return (transform + Affine2D() .scale(POINTS_PER_INCH/self.dpi) .scale(1.0, -1.0) .translate(0.0, POINTS_PER_INCH*self.get_figheight()))
def analyze_foil( date, shot, angular_band=20., safe_radius=5., file_name="square.png", add_scalebar=True, rotate_image=False, ): spatial_info = DF_SPATIAL[(DF_SPATIAL["date"] == date) & (DF_SPATIAL["shot"] == shot)] image_path = os.path.join(dir.get_drive("d"), "Data", "Processed", "Soot Foil", "foil images", date, f"Shot {shot:02d}", file_name) if not os.path.exists(image_path): # reads better with this at the top raise FileExistsError(f"{image_path} does not exist") else: fft_pass = [ angular_band, # angular band (+/-) safe_radius, # safe radius ] delta_px = spatial_info["delta_px"].values[0] delta_mm = spatial_info["delta_mm"].values[0] to_keep = 10 # run analysis df_cells, plot_args = spectral.analysis.run( image_path, fft_pass, delta_px, delta_mm, bg_subtract=spatial_info["bg_subtract"].values[0], to_keep=to_keep, return_plot_outputs=True, ) fig_img, ax_img = plot.image_filtering(*plot_args["image_filtering"], rotate_image=rotate_image) # _ = spectral.plot.scans(*plot_args["scans"]) _ = plot.measurements(*plot_args["measurements"]) from matplotlib.transforms import Affine2D r = Affine2D().rotate_deg(90) t = ax_img[0][0].get_transform() ax_img[0][0].set_transform(r + t) ax_img = ax_img.flatten() if add_scalebar: if rotate_image: scalebar_rotation = "vertical" scalebar_location = 3 else: scalebar_rotation = "horizontal" scalebar_location = None for a in ax_img[[0, 3, 4]]: scalebar = ScaleBar( delta_mm / delta_px, "mm", fixed_value=df_cells["Cell Size"].values[0], label_formatter=(lambda x, u: f"{x:.2f} {u}"), border_pad=0.2, color="#FFFFFF", box_color="#444444", box_alpha=0, rotation=scalebar_rotation, location=scalebar_location, ) a.add_artist(scalebar) return df_cells
def update_image_abs_coords(image, poses, lidar_points, self_position, only_nearby_meters, figsize=None, tail_points=None, tail_lines=None, lines=None): if figsize is None: figsize = figsize_from_image_size(image) poses_x_points = [x['x'] for x in poses] poses_y_points = [x['y'] for x in poses] if len(poses) == 0: pose = {'x': 0., 'y': 0., 'teta': 0.} else: pose = poses[-1] fig = Figure(figsize=figsize) canvas = FigureCanvas(fig) ax = fig.gca() ax.set_xlim(-only_nearby_meters, only_nearby_meters) ax.set_ylim(-only_nearby_meters, only_nearby_meters) ax.scatter(poses_x_points, poses_y_points, marker='o', s=1, c='gray') if tail_points: tail_x_points = [x[0] for x in tail_points] tail_y_points = [x[1] for x in tail_points] ax.scatter(tail_x_points, tail_y_points, marker='o', s=5, c='lightblue') if tail_lines: for a_line in tail_lines: x_points = [a_line[0][0], a_line[1][0]] y_points = [a_line[0][1], a_line[1][1]] ax.plot(x_points, y_points, c='lightblue') lidar_x_points = [x[0] for x in lidar_points] lidar_y_points = [x[1] for x in lidar_points] ax.scatter(lidar_x_points, lidar_y_points, marker='o', s=5, c='blue') if lines: for a_line in lines: x_points = [a_line[0][0], a_line[1][0]] y_points = [a_line[0][1], a_line[1][1]] ax.plot(x_points, y_points, c='orange', linewidth=3) if self_position is not None: rect = patches.Rectangle( (self_position['x'] + pose['x'], self_position['y'] + pose['y']), self_position['w'], self_position['h'], linewidth=3, edgecolor='r', facecolor='none') rotation_center = ax.transData.transform([ self_position['x'] + pose['x'] + self_position['w'] / 2, self_position['y'] + pose['y'] + self_position['h'] / 2, ]) rotation = Affine2D().rotate_around(rotation_center[0], rotation_center[1], pose['teta']) rect.set_transform(ax.transData + rotation) ax.add_patch(rect) else: ax.scatter([pose['x']], [pose['y']], marker='o', s=50, c='r') ax.text(-only_nearby_meters, -only_nearby_meters, 'x: {:0.02f}, y:{:0.02f}, teta: {:0.02f}, pose {}'.format( pose['x'], pose['y'], pose['teta'], len(poses)), fontsize=10) ax.grid(which='both', linestyle='--', alpha=0.5) canvas.draw() jpeg = BytesIO() canvas.print_jpg(jpeg) image.value = jpeg.getvalue()
def create_cg(fig=None, subplot=111, rot=-450, scale=-1, angular_spacing=10, radial_spacing=10, latmin=0, lon_cycle=360): """ Helper function to create curvelinear grid The function makes use of the Matplotlib AXISARTIST namespace `mpl_toolkits.axisartist \ <https://matplotlib.org/mpl_toolkits/axes_grid/users/axisartist.html>`_. Here are some limitations to normal Matplotlib Axes. While using the Matplotlib `AxesGrid Toolkit \ <https://matplotlib.org/mpl_toolkits/axes_grid/index.html>`_ most of the limitations can be overcome. See `Matplotlib AxesGrid Toolkit User’s Guide \ <https://matplotlib.org/mpl_toolkits/axes_grid/users/index.html>`_. Parameters ---------- fig : matplotlib Figure object If given, the PPI/RHI will be plotted into this figure object. Axes are created as needed. If None a new figure object will be created or current figure will be used, depending on "subplot". subplot : :class:`matplotlib:matplotlib.gridspec.GridSpec`, \ matplotlib grid definition nrows/ncols/plotnumber, see examples section defaults to '111', only one subplot rot : float Rotation of the source data in degrees, defaults to -450 for PPI, use 0 for RHI scale : float Scale of source data, defaults to -1. for PPI, use 1 for RHI angular_spacing : float Spacing of the angular grid, defaults to 10. radial_spacing : float Spacing of the radial grid, defaults to 10. latmin : float Startvalue for radial grid, defaults to 0. lon_cycle : float Angular cycle, defaults to 360. Returns ------- cgax : matplotlib toolkit axisartist Axes object curvelinear Axes (r-theta-grid) caax : matplotlib Axes object (twin to cgax) Cartesian Axes (x-y-grid) for plotting cartesian data paax : matplotlib Axes object (parasite to cgax) The parasite axes object for plotting polar data """ # create transformation # rotate tr_rotate = Affine2D().translate(rot, 0) # scale tr_scale = Affine2D().scale(scale * np.pi / 180, 1) # polar tr_polar = PolarAxes.PolarTransform() tr = tr_rotate + tr_scale + tr_polar # build up curvelinear grid extreme_finder = ah.ExtremeFinderCycle( 360, 360, lon_cycle=lon_cycle, lat_cycle=None, lon_minmax=None, lat_minmax=(latmin, np.inf), ) # locator and formatter for angular annotation grid_locator1 = ah.LocatorDMS(lon_cycle // angular_spacing) tick_formatter1 = ah.FormatterDMS() # grid_helper for curvelinear grid grid_helper = GridHelperCurveLinear( tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, grid_locator2=None, tick_formatter1=tick_formatter1, tick_formatter2=None, ) # try to set nice locations for radial gridlines grid_locator2 = grid_helper.grid_finder.grid_locator2 grid_locator2._nbins = (radial_spacing * 2 + 1) // np.sqrt(2) # if there is no figure object given if fig is None: # create new figure if there is only one subplot if subplot == 111: fig = pl.figure() # otherwise get current figure or create new figure else: fig = pl.gcf() # generate Axis cgax = SubplotHost(fig, subplot, grid_helper=grid_helper) fig.add_axes(cgax) # get twin axis for cartesian grid caax = cgax.twin() # move axis annotation from right to left and top to bottom for # cartesian axis caax.toggle_axisline() # make right and top axis visible and show ticklabels (curvelinear axis) cgax.axis["top", "right"].set_visible(True) cgax.axis["top", "right"].major_ticklabels.set_visible(True) # make ticklabels of left and bottom axis invisible (curvelinear axis) cgax.axis["left", "bottom"].major_ticklabels.set_visible(False) # and also set tickmarklength to zero for better presentation # (curvelinear axis) cgax.axis["top", "right", "left", "bottom"].major_ticks.set_ticksize(0) # show theta (angles) on top and right axis cgax.axis["top"].get_helper().nth_coord_ticks = 0 cgax.axis["right"].get_helper().nth_coord_ticks = 0 # generate and add parasite axes with given transform paax = ParasiteAxesAuxTrans(cgax, tr, "equal") # note that paax.transData == tr + cgax.transData # Anything you draw in paax will match the ticks and grids of cgax. cgax.parasites.append(paax) return cgax, caax, paax
fig = plt.figure(figsize=(8, 8)) # Main axis ax1 = plt.subplot(1, 1, 1, aspect=1, xlim=[0, 10], ylim=[0, 10]) ax1.xaxis.set_major_locator(MultipleLocator(1.00)) ax1.xaxis.set_minor_locator(MultipleLocator(0.50)) ax1.yaxis.set_major_locator(MultipleLocator(1.00)) ax1.yaxis.set_minor_locator(MultipleLocator(0.50)) ax1.grid(linewidth=0.75, linestyle="--") # Floating axis center = np.array([5, 5]) # data coordinates size = np.array([5, 3]) # data coordinates orientation = -30 # degrees T = size / 2 * [(-1, -1), (+1, -1), (+1, +1), (-1, +1), (-1, -1)] rotation = Affine2D().rotate_deg(orientation) P = center + rotation.transform(T) # Floating axis bounding box visualization # T = rotation.transform(T) # xmin, xmax = T[:,0].min(), T[:,0].max() # ymin, ymax = T[:,1].min(), T[:,1].max() # T = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin)] # P = center + T # plt.plot(P[:,0], P[:,1], color="black", linewidth=0.75) # Actual floating axis DC_to_FC = ax1.transData.transform FC_to_NFC = fig.transFigure.inverted().transform DC_to_NFC = lambda x: FC_to_NFC(DC_to_FC(x)) xmin, ymin = DC_to_NFC((P[:, 0].min(), P[:, 1].min()))
def draw_image(self, gc, x, y, im, transform=None): # docstring inherited h, w = im.shape[:2] if w == 0 or h == 0: return attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) oid = gc.get_gid() url = gc.get_url() if url is not None: self.writer.start('a', attrib={'xlink:href': url}) if rcParams['svg.image_inline']: bytesio = io.BytesIO() _png.write_png(im, bytesio) oid = oid or self._make_id('image', bytesio.getvalue()) attrib['xlink:href'] = ( "data:image/png;base64,\n" + base64.b64encode(bytesio.getvalue()).decode('ascii')) else: self._imaged[self.basename] = (self._imaged.get(self.basename, 0) + 1) filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) _log.info('Writing image file for inclusion: %s', filename) _png.write_png(im, filename) oid = oid or 'Im_' + self._make_id('image', filename) attrib['xlink:href'] = filename attrib['id'] = oid if transform is None: w = 72.0 * w / self.image_dpi h = 72.0 * h / self.image_dpi self.writer.element('image', transform=generate_transform([ ('scale', (1, -1)), ('translate', (0, -h)) ]), x=short_float_fmt(x), y=short_float_fmt(-(self.height - y - h)), width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) else: alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = short_float_fmt(alpha) flipped = (Affine2D().scale(1.0 / w, 1.0 / h) + transform + Affine2D().translate(x, y).scale(1.0, -1.0).translate( 0.0, self.height)) attrib['transform'] = generate_transform([('matrix', flipped.frozen())]) self.writer.element('image', width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) if url is not None: self.writer.end('a') if clipid is not None: self.writer.end('g')
def add(self, patchlabel='', flows=None, orientations=None, labels='', trunklength=1.0, pathlengths=0.25, prior=None, connect=(0, 0), rotation=0, **kwargs): """ Add a simple Sankey diagram with flows at the same hierarchical level. Return value is the instance of :class:`Sankey`. Optional keyword arguments: =============== =================================================== Keyword Description =============== =================================================== *patchlabel* label to be placed at the center of the diagram Note: *label* (not *patchlabel*) will be passed to the patch through ``**kwargs`` and can be used to create an entry in the legend. *flows* array of flow values By convention, inputs are positive and outputs are negative. *orientations* list of orientations of the paths Valid values are 1 (from/to the top), 0 (from/to the left or right), or -1 (from/to the bottom). If *orientations* == 0, inputs will break in from the left and outputs will break away to the right. *labels* list of specifications of the labels for the flows Each value may be *None* (no labels), '' (just label the quantities), or a labeling string. If a single value is provided, it will be applied to all flows. If an entry is a non-empty string, then the quantity for the corresponding flow will be shown below the string. However, if the *unit* of the main diagram is None, then quantities are never shown, regardless of the value of this argument. *trunklength* length between the bases of the input and output groups *pathlengths* list of lengths of the arrows before break-in or after break-away If a single value is given, then it will be applied to the first (inside) paths on the top and bottom, and the length of all other arrows will be justified accordingly. The *pathlengths* are not applied to the horizontal inputs and outputs. *prior* index of the prior diagram to which this diagram should be connected *connect* a (prior, this) tuple indexing the flow of the prior diagram and the flow of this diagram which should be connected If this is the first diagram or *prior* is *None*, *connect* will be ignored. *rotation* angle of rotation of the diagram [deg] *rotation* is ignored if this diagram is connected to an existing one (using *prior* and *connect*). The interpretation of the *orientations* argument will be rotated accordingly (e.g., if *rotation* == 90, an *orientations* entry of 1 means to/from the left). =============== =================================================== Valid kwargs are :meth:`matplotlib.patches.PathPatch` arguments: %(Patch)s As examples, ``fill=False`` and ``label='A legend entry'``. By default, ``facecolor='#bfd1d4'`` (light blue) and ``linewidth=0.5``. The indexing parameters (*prior* and *connect*) are zero-based. The flows are placed along the top of the diagram from the inside out in order of their index within the *flows* list or array. They are placed along the sides of the diagram from the top down and along the bottom from the outside in. If the sum of the inputs and outputs is nonzero, the discrepancy will appear as a cubic Bezier curve along the top and bottom edges of the trunk. .. seealso:: :meth:`finish` """ # Check and preprocess the arguments. if flows is None: flows = np.array([1.0, -1.0]) else: flows = np.array(flows) n = flows.shape[0] # Number of flows if rotation is None: rotation = 0 else: # In the code below, angles are expressed in deg/90. rotation /= 90.0 if orientations is None: orientations = [0, 0] if len(orientations) != n: raise ValueError( "orientations and flows must have the same length.\n" "orientations has length %d, but flows has length %d." % (len(orientations), n)) if labels != '' and getattr(labels, '__iter__', False): # iterable() isn't used because it would give True if labels is a # string if len(labels) != n: raise ValueError( "If labels is a list, then labels and flows must have the " "same length.\nlabels has length %d, but flows has length %d." % (len(labels), n)) else: labels = [labels] * n if trunklength < 0: raise ValueError( "trunklength is negative.\nThis isn't allowed, because it would " "cause poor layout.") if np.abs(np.sum(flows)) > self.tolerance: verbose.report( "The sum of the flows is nonzero (%f).\nIs the " "system not at steady state?" % np.sum(flows), 'helpful') scaled_flows = self.scale * flows gain = sum(max(flow, 0) for flow in scaled_flows) loss = sum(min(flow, 0) for flow in scaled_flows) if not (0.5 <= gain <= 2.0): verbose.report( "The scaled sum of the inputs is %f.\nThis may " "cause poor layout.\nConsider changing the scale so" " that the scaled sum is approximately 1.0." % gain, 'helpful') if not (-2.0 <= loss <= -0.5): verbose.report( "The scaled sum of the outputs is %f.\nThis may " "cause poor layout.\nConsider changing the scale so" " that the scaled sum is approximately 1.0." % gain, 'helpful') if prior is not None: if prior < 0: raise ValueError("The index of the prior diagram is negative.") if min(connect) < 0: raise ValueError( "At least one of the connection indices is negative.") if prior >= len(self.diagrams): raise ValueError( "The index of the prior diagram is %d, but there are " "only %d other diagrams.\nThe index is zero-based." % (prior, len(self.diagrams))) if connect[0] >= len(self.diagrams[prior].flows): raise ValueError( "The connection index to the source diagram is %d, but " "that diagram has only %d flows.\nThe index is zero-based." % (connect[0], len(self.diagrams[prior].flows))) if connect[1] >= n: raise ValueError( "The connection index to this diagram is %d, but this diagram" "has only %d flows.\n The index is zero-based." % (connect[1], n)) if self.diagrams[prior].angles[connect[0]] is None: raise ValueError( "The connection cannot be made. Check that the magnitude " "of flow %d of diagram %d is greater than or equal to the " "specified tolerance." % (connect[0], prior)) flow_error = (self.diagrams[prior].flows[connect[0]] + flows[connect[1]]) if abs(flow_error) >= self.tolerance: raise ValueError( "The scaled sum of the connected flows is %f, which is not " "within the tolerance (%f)." % (flow_error, self.tolerance)) # Determine if the flows are inputs. are_inputs = [None] * n for i, flow in enumerate(flows): if flow >= self.tolerance: are_inputs[i] = True elif flow <= -self.tolerance: are_inputs[i] = False else: verbose.report( "The magnitude of flow %d (%f) is below the " "tolerance (%f).\nIt will not be shown, and it " "cannot be used in a connection." % (i, flow, self.tolerance), 'helpful') # Determine the angles of the arrows (before rotation). angles = [None] * n for i, (orient, is_input) in enumerate(zip(orientations, are_inputs)): if orient == 1: if is_input: angles[i] = DOWN elif not is_input: # Be specific since is_input can be None. angles[i] = UP elif orient == 0: if is_input is not None: angles[i] = RIGHT else: if orient != -1: raise ValueError("The value of orientations[%d] is %d, " "but it must be [ -1 | 0 | 1 ]." % (i, orient)) if is_input: angles[i] = UP elif not is_input: angles[i] = DOWN # Justify the lengths of the paths. if iterable(pathlengths): if len(pathlengths) != n: raise ValueError( "If pathlengths is a list, then pathlengths and flows must " "have the same length.\npathlengths has length %d, but flows " "has length %d." % (len(pathlengths), n)) else: # Make pathlengths into a list. urlength = pathlengths ullength = pathlengths lrlength = pathlengths lllength = pathlengths d = dict(RIGHT=pathlengths) pathlengths = [d.get(angle, 0) for angle in angles] # Determine the lengths of the top-side arrows # from the middle outwards. for i, (angle, is_input, flow) in enumerate(zip(angles, are_inputs, scaled_flows)): if angle == DOWN and is_input: pathlengths[i] = ullength ullength += flow elif angle == UP and not is_input: pathlengths[i] = urlength urlength -= flow # Flow is negative for outputs. # Determine the lengths of the bottom-side arrows # from the middle outwards. for i, (angle, is_input, flow) in enumerate( reversed(list(zip(angles, are_inputs, scaled_flows)))): if angle == UP and is_input: pathlengths[n - i - 1] = lllength lllength += flow elif angle == DOWN and not is_input: pathlengths[n - i - 1] = lrlength lrlength -= flow # Determine the lengths of the left-side arrows # from the bottom upwards. has_left_input = False for i, (angle, is_input, spec) in enumerate( reversed( list( zip(angles, are_inputs, zip(scaled_flows, pathlengths))))): if angle == RIGHT: if is_input: if has_left_input: pathlengths[n - i - 1] = 0 else: has_left_input = True # Determine the lengths of the right-side arrows # from the top downwards. has_right_output = False for i, (angle, is_input, spec) in enumerate( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == RIGHT: if not is_input: if has_right_output: pathlengths[i] = 0 else: has_right_output = True # Begin the subpaths, and smooth the transition if the sum of the flows # is nonzero. urpath = [ ( Path.MOVETO, [ (self.gap - trunklength / 2.0), # Upper right gain / 2.0 ]), (Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0, gain / 2.0]), (Path.CURVE4, [(self.gap - trunklength / 2.0) / 8.0, gain / 2.0]), (Path.CURVE4, [(trunklength / 2.0 - self.gap) / 8.0, -loss / 2.0]), (Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0, -loss / 2.0]), (Path.LINETO, [(trunklength / 2.0 - self.gap), -loss / 2.0]) ] llpath = [ ( Path.LINETO, [ (trunklength / 2.0 - self.gap), # Lower left loss / 2.0 ]), (Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0, loss / 2.0]), (Path.CURVE4, [(trunklength / 2.0 - self.gap) / 8.0, loss / 2.0]), (Path.CURVE4, [(self.gap - trunklength / 2.0) / 8.0, -gain / 2.0]), (Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0, -gain / 2.0]), (Path.LINETO, [(self.gap - trunklength / 2.0), -gain / 2.0]) ] lrpath = [( Path.LINETO, [ (trunklength / 2.0 - self.gap), # Lower right loss / 2.0 ])] ulpath = [( Path.LINETO, [ self.gap - trunklength / 2.0, # Upper left gain / 2.0 ])] # Add the subpaths and assign the locations of the tips and labels. tips = np.zeros((n, 2)) label_locations = np.zeros((n, 2)) # Add the top-side inputs and outputs from the middle outwards. for i, (angle, is_input, spec) in enumerate( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == DOWN and is_input: tips[i, :], label_locations[i, :] = self._add_input( ulpath, angle, *spec) elif angle == UP and not is_input: tips[i, :], label_locations[i, :] = self._add_output( urpath, angle, *spec) # Add the bottom-side inputs and outputs from the middle outwards. for i, (angle, is_input, spec) in enumerate( reversed( list( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))))): if angle == UP and is_input: tip, label_location = self._add_input(llpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location elif angle == DOWN and not is_input: tip, label_location = self._add_output(lrpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location # Add the left-side inputs from the bottom upwards. has_left_input = False for i, (angle, is_input, spec) in enumerate( reversed( list( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))))): if angle == RIGHT and is_input: if not has_left_input: # Make sure the lower path extends # at least as far as the upper one. if llpath[-1][1][0] > ulpath[-1][1][0]: llpath.append( (Path.LINETO, [ulpath[-1][1][0], llpath[-1][1][1]])) has_left_input = True tip, label_location = self._add_input(llpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location # Add the right-side outputs from the top downwards. has_right_output = False for i, (angle, is_input, spec) in enumerate( zip(angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == RIGHT and not is_input: if not has_right_output: # Make sure the upper path extends # at least as far as the lower one. if urpath[-1][1][0] < lrpath[-1][1][0]: urpath.append( (Path.LINETO, [lrpath[-1][1][0], urpath[-1][1][1]])) has_right_output = True tips[i, :], label_locations[i, :] = self._add_output( urpath, angle, *spec) # Trim any hanging vertices. if not has_left_input: ulpath.pop() llpath.pop() if not has_right_output: lrpath.pop() urpath.pop() # Concatenate the subpaths in the correct order (clockwise from top). path = (urpath + self._revert(lrpath) + llpath + self._revert(ulpath) + [(Path.CLOSEPOLY, urpath[0][1])]) # Create a patch with the Sankey outline. codes, vertices = list(zip(*path)) vertices = np.array(vertices) def _get_angle(a, r): if a is None: return None else: return a + r if prior is None: if rotation != 0: # By default, none of this is needed. angles = [_get_angle(angle, rotation) for angle in angles] rotate = Affine2D().rotate_deg(rotation * 90).transform_affine tips = rotate(tips) label_locations = rotate(label_locations) vertices = rotate(vertices) text = self.ax.text(0, 0, s=patchlabel, ha='center', va='center') else: rotation = (self.diagrams[prior].angles[connect[0]] - angles[connect[1]]) angles = [_get_angle(angle, rotation) for angle in angles] rotate = Affine2D().rotate_deg(rotation * 90).transform_affine tips = rotate(tips) offset = self.diagrams[prior].tips[connect[0]] - tips[connect[1]] translate = Affine2D().translate(*offset).transform_affine tips = translate(tips) label_locations = translate(rotate(label_locations)) vertices = translate(rotate(vertices)) kwds = dict(s=patchlabel, ha='center', va='center') text = self.ax.text(*offset, **kwds) if False: # Debug print("llpath\n", llpath) print("ulpath\n", self._revert(ulpath)) print("urpath\n", urpath) print("lrpath\n", self._revert(lrpath)) xs, ys = list(zip(*vertices)) self.ax.plot(xs, ys, 'go-') if rcParams['_internal.classic_mode']: fc = kwargs.pop('fc', kwargs.pop('facecolor', '#bfd1d4')) lw = kwargs.pop('lw', kwargs.pop('linewidth', 0.5)) else: fc = kwargs.pop('fc', kwargs.pop('facecolor', None)) lw = kwargs.pop('lw', kwargs.pop('linewidth', None)) if fc is None: fc = six.next(self.ax._get_patches_for_fill.prop_cycler)['color'] patch = PathPatch(Path(vertices, codes), fc=fc, lw=lw, **kwargs) self.ax.add_patch(patch) # Add the path labels. texts = [] for number, angle, label, location in zip(flows, angles, labels, label_locations): if label is None or angle is None: label = '' elif self.unit is not None: quantity = self.format % abs(number) + self.unit if label != '': label += "\n" label += quantity texts.append( self.ax.text(x=location[0], y=location[1], s=label, ha='center', va='center')) # Text objects are placed even they are empty (as long as the magnitude # of the corresponding flow is larger than the tolerance) in case the # user wants to provide labels later. # Expand the size of the diagram if necessary. self.extent = (min(np.min(vertices[:, 0]), np.min(label_locations[:, 0]), self.extent[0]), max(np.max(vertices[:, 0]), np.max(label_locations[:, 0]), self.extent[1]), min(np.min(vertices[:, 1]), np.min(label_locations[:, 1]), self.extent[2]), max(np.max(vertices[:, 1]), np.max(label_locations[:, 1]), self.extent[3])) # Include both vertices _and_ label locations in the extents; there are # where either could determine the margins (e.g., arrow shoulders). # Add this diagram as a subdiagram. self.diagrams.append( Bunch(patch=patch, flows=flows, angles=angles, tips=tips, text=text, texts=texts)) # Allow a daisy-chained call structure (see docstring for the class). return self
def _set_lim_and_transforms(self): # A (possibly non-linear) projection on the (already scaled) data # There are three important coordinate spaces going on here: # # 1. Data space: The space of the data itself # # 2. Axes space: The unit rectangle (0, 0) to (1, 1) # covering the entire plot area. # # 3. Display space: The coordinates of the resulting image, # often in pixels or dpi/inch. # This function makes heavy use of the Transform classes in # ``lib/matplotlib/transforms.py.`` For more information, see # the inline documentation there. # The goal of the first two transformations is to get from the # data space (in this case longitude and latitude) to axes # space. It is separated into a non-affine and affine part so # that the non-affine part does not have to be recomputed when # a simple affine change to the figure has been made (such as # resizing the window or changing the dpi). # 1) The core transformation from data space into # rectilinear space defined in the HammerTransform class. self.transProjection = self._get_core_transform(self.RESOLUTION) # 2) The above has an output range that is not in the unit # rectangle, so scale and translate it so it fits correctly # within the axes. The peculiar calculations of xscale and # yscale are specific to a Aitoff-Hammer projection, so don't # worry about them too much. self.transAffine = self._get_affine_transform() # 3) This is the transformation from axes space to display # space. self.transAxes = BboxTransformTo(self.bbox) # Now put these 3 transforms together -- from data all the way # to display coordinates. Using the '+' operator, these # transforms will be applied "in order". The transforms are # automatically simplified, if possible, by the underlying # transformation framework. self.transData = \ self.transProjection + \ self.transAffine + \ self.transAxes # The main data transformation is set up. Now deal with # gridlines and tick labels. # Longitude gridlines and ticklabels. The input to these # transforms are in display space in x and axes space in y. # Therefore, the input values will be in range (-xmin, 0), # (xmax, 1). The goal of these transforms is to go from that # space to display space. The tick labels will be offset 4 # pixels from the equator. self._xaxis_pretransform = \ Affine2D() \ .scale(1.0, self._longitude_cap * 2.0) \ .translate(0.0, -self._longitude_cap) self._xaxis_transform = \ self._xaxis_pretransform + \ self.transData self._xaxis_text1_transform = \ Affine2D().scale(1.0, 0.0) + \ self.transData + \ Affine2D().translate(0.0, 4.0) self._xaxis_text2_transform = \ Affine2D().scale(1.0, 0.0) + \ self.transData + \ Affine2D().translate(0.0, -4.0) # Now set up the transforms for the latitude ticks. The input to # these transforms are in axes space in x and display space in # y. Therefore, the input values will be in range (0, -ymin), # (1, ymax). The goal of these transforms is to go from that # space to display space. The tick labels will be offset 4 # pixels from the edge of the axes ellipse. yaxis_stretch = Affine2D().scale(np.pi * 2, 1).translate(-np.pi, 0) yaxis_space = Affine2D().scale(1.0, 1.1) self._yaxis_transform = \ yaxis_stretch + \ self.transData yaxis_text_base = \ yaxis_stretch + \ self.transProjection + \ (yaxis_space + self.transAffine + self.transAxes) self._yaxis_text1_transform = \ yaxis_text_base + \ Affine2D().translate(-8.0, 0.0) self._yaxis_text2_transform = \ yaxis_text_base + \ Affine2D().translate(8.0, 0.0)
def path(self): path = Path.arc(self.start_angle, self.end_angle) transform = Affine2D().scale(self.radius).translate(*self.center.xy()) return path.transformed(transform)