Matplotlib label axis: how to determine where they will be located?

I wrote a routine for drawing vertical sections from the output of an atmospheric model. An example is shown below. What I would like to do is to show two vertical axes: on the left I show the pressure values ​​on the log scale, and on the right I show the height in km. I thought it would be nice to show the height in the places of the model levels - that’s why they are irregularly spaced. Everything works beautifully, except that the labels on the right overlap at the base. I found out that I can hide certain tags using ax2.get_yticklabels()[index].set_visible(False) . My problem: how to determine which labels (indexes) I want to hide? I believe it should be possible to figure out where the cue marks are located (in the coordinates of an axis or shape). Then I could use the threshold distance, as in

 yp = -1 for t in ax2.get_yticklabels(): y = t.get_position().y0 # this doesn't yield any useful bbox! if y-yp < threshold: t.set_visible(False) else: yp = y 

Unfortunately, I did not find a way to get the coordinates of the label. Any clues?

Here is an example: Vertical cross section plot

And here is the complete code that performs the construction (the data is a two-dimensional array, x - latitudes, and y - pressure values):

 def plotZM(data, x, y, plotOpt=None): """Create a zonal mean contour plot of one variable plotOpt is a dictionary with plotting options: 'scale_factor': multiply values with this factor before plotting 'units': a units label for the colorbar 'levels': use list of values as contour intervals 'title': a title for the plot """ if plotOpt is None: plotOpt = {} # create figure and axes fig = plt.figure() ax1 = fig.add_subplot(111) # scale data if requested scale_factor = plotOpt.get('scale_factor', 1.0) pdata = data * scale_factor # determine contour levels to be used; default: linear spacing, 20 levels clevs = plotOpt.get('levels', np.linspace(data.min(), data.max(), 20)) # map contour values to colors norm=matplotlib.colors.BoundaryNorm(clevs, ncolors=256, clip=False) # draw the (filled) contours contour = ax1.contourf(x, y, pdata, levels=clevs, norm=norm) # add a title title = plotOpt.get('title', 'Vertical cross section') ax1.set_title(title) # optional keyword: fontsize="small" # add colorbar # Note: use of the ticks keyword forces colorbar to draw all labels fmt = matplotlib.ticker.FormatStrFormatter("%g") cbar = fig.colorbar(contour, ax=ax1, orientation='horizontal', shrink=0.8, ticks=clevs, format=fmt) cbar.set_label(plotOpt.get('units', '')) for t in cbar.ax.get_xticklabels(): t.set_fontsize("x-small") # change font size of x labels xlabels = ax1.get_xticklabels() for t in xlabels: t.set_fontsize("x-small") # set up y axes: log pressure labels on the left y axis, altitude labels # according to model levels on the right y axis ax1.set_ylabel("Pressure [hPa]") ax1.set_yscale('log') ax1.set_ylim(y.max(), y.min()) subs = [1,2,5] print "y_max/y_min = ", y.max()/y.min() if y.max()/y.min() < 30.: subs = [1,2,3,4,5,6,7,8,9] loc = matplotlib.ticker.LogLocator(base=10., subs=subs) ax1.yaxis.set_major_locator(loc) fmt = matplotlib.ticker.FormatStrFormatter("%g") ax1.yaxis.set_major_formatter(fmt) ylabels = ax1.get_yticklabels() for t in ylabels: t.set_fontsize("x-small") # calculate altitudes from pressure values (use fixed scale height) z0 = 8.400 # scale height for pressure_to_altitude conversion [km] altitude = z0 * np.log(1015.23/y) # add second y axis for altitude scale ax2 = ax1.twinx() ax2.set_ylabel("Altitude [km]") ax2.set_ylim(altitude.min(), altitude.max()) ax2.set_yticks(altitude) fmt = matplotlib.ticker.FormatStrFormatter("%6.1f") ax2.yaxis.set_major_formatter(fmt) # tweak altitude labels ylabels = ax2.get_yticklabels() for i,t in enumerate(ylabels): t.set_fontsize("x-small") # show plot plt.show() 
+6
source share
2 answers

I would rather not do such things. This can cause problems, for example, when changing the size of the graph window or when changing the dpi of the output images. And it will definitely look awkward.

You must either

  • set fewer ticks on ax2 (and in my opinion they should be on a linear scale for height
  • Use the predefined (or even custom) locator from matplotlib. I like AutoLocator and MaxNLocator, just try which one will give you the most enjoyable results.

If you need help with this, just ask.

+2
source

Below is an updated version of plotZM, which will display the model levels in a separate panel on the right and use linear equidistant markers for the height axis. Added another option to mask areas below surface pressure.

This code is β€œscalable” (that is, elevation and pressure labels change well with zooming or panning, and model levels are constantly changing). It also contains a rather complicated axis and label setting, and therefore can be useful to others as a more complex example of what you can do with matplotlib. Below is an example of a picture.

 def plotZM(data, x, y, plotOpt=None, modelLevels=None, surfacePressure=None): """Create a zonal mean contour plot of one variable plotOpt is a dictionary with plotting options: 'scale_factor': multiply values with this factor before plotting 'units': a units label for the colorbar 'levels': use list of values as contour intervals 'title': a title for the plot modelLevels: a list of pressure values indicating the model vertical resolution. If present, a small side panel will be drawn with lines for each model level surfacePressure: a list (dimension len(x)) of surface pressure values. If present, these will be used to mask out regions below the surface """ # explanation of axes: # ax1: primary coordinate system latitude vs. pressure (left ticks on y axis) # ax2: twinned axes for altitude coordinates on right y axis # axm: small side panel with shared y axis from ax2 for display of model levels # right y ticks and y label will be drawn on axr if modelLevels are given, else on ax2 # axr: pointer to "right axis", either ax2 or axm if plotOpt is None: plotOpt = {} labelFontSize = "small" # create figure and axes fig = plt.figure() ax1 = fig.add_subplot(111) # scale data if requested scale_factor = plotOpt.get('scale_factor', 1.0) pdata = data * scale_factor # determine contour levels to be used; default: linear spacing, 20 levels clevs = plotOpt.get('levels', np.linspace(data.min(), data.max(), 20)) # map contour values to colors norm=matplotlib.colors.BoundaryNorm(clevs, ncolors=256, clip=False) # draw the (filled) contours contour = ax1.contourf(x, y, pdata, levels=clevs, norm=norm) # mask out surface pressure if given if not surfacePressure is None: ax1.fill_between(x, surfacePressure, surfacePressure.max(), color="white") # add a title title = plotOpt.get('title', 'Vertical cross section') ax1.set_title(title) # add colorbar # Note: use of the ticks keyword forces colorbar to draw all labels fmt = matplotlib.ticker.FormatStrFormatter("%g") cbar = fig.colorbar(contour, ax=ax1, orientation='horizontal', shrink=0.8, ticks=clevs, format=fmt) cbar.set_label(plotOpt.get('units', '')) for t in cbar.ax.get_xticklabels(): t.set_fontsize(labelFontSize) # set up y axes: log pressure labels on the left y axis, altitude labels # according to model levels on the right y axis ax1.set_ylabel("Pressure [hPa]") ax1.set_yscale('log') ax1.set_ylim(10.*np.ceil(y.max()/10.), y.min()) # avoid truncation of 1000 hPa subs = [1,2,5] if y.max()/y.min() < 30.: subs = [1,2,3,4,5,6,7,8,9] y1loc = matplotlib.ticker.LogLocator(base=10., subs=subs) ax1.yaxis.set_major_locator(y1loc) fmt = matplotlib.ticker.FormatStrFormatter("%g") ax1.yaxis.set_major_formatter(fmt) for t in ax1.get_yticklabels(): t.set_fontsize(labelFontSize) # calculate altitudes from pressure values (use fixed scale height) z0 = 8.400 # scale height for pressure_to_altitude conversion [km] altitude = z0 * np.log(1015.23/y) # add second y axis for altitude scale ax2 = ax1.twinx() # change values and font size of x labels ax1.set_xlabel('Latitude [degrees]') xloc = matplotlib.ticker.FixedLocator(np.arange(-90.,91.,30.)) ax1.xaxis.set_major_locator(xloc) for t in ax1.get_xticklabels(): t.set_fontsize(labelFontSize) # draw horizontal lines to the right to indicate model levels if not modelLevels is None: pos = ax1.get_position() axm = fig.add_axes([pos.x1,pos.y0,0.02,pos.height], sharey=ax2) axm.set_xlim(0., 1.) axm.xaxis.set_visible(False) modelLev = axm.hlines(altitude, 0., 1., color='0.5') axr = axm # specify y axis for right tick marks and labels # turn off tick labels of ax2 for t in ax2.get_yticklabels(): t.set_visible(False) label_xcoor = 3.7 else: axr = ax2 label_xcoor = 1.05 axr.set_ylabel("Altitude [km]") axr.yaxis.set_label_coords(label_xcoor, 0.5) axr.set_ylim(altitude.min(), altitude.max()) yrloc = matplotlib.ticker.MaxNLocator(steps=[1,2,5,10]) axr.yaxis.set_major_locator(yrloc) axr.yaxis.tick_right() for t in axr.yaxis.get_majorticklines(): t.set_visible(False) for t in axr.get_yticklabels(): t.set_fontsize(labelFontSize) # show plot plt.show() 

vertical cross section plot with modified code

+2
source

All Articles