Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

matplotlib contour plot labels overlap axes

I'm making some contour plots with contour which are labeled via clabel. The problem is that the contour labels tend to overlap with the axes: enter image description here

(some of the other labels are messy, ignore that). For the left plot, 10^-3 and 10 are problematic. On the right, 10^3 is the only problem one. Here is the code that generates one of them:

fig = plt.figure(figsize=(6,3))
ax = fig.add_subplot(121)
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel(r'$T_e$ (eV)', fontsize=10)
ax.set_ylabel(r'$n_e$ (1/cm$^3$)', fontsize=10)
ax.set_xlim(0.1, 1e4)
ax.set_ylim(1e16, 1e28)
CS = ax.contour(X, Y, Z, V, colors='k')
ax.clabel(CS, inline=True, inline_spacing=3, rightside_up=True, colors='k', fontsize=8, fmt=fmt)

Is there any way to get clabel to be better behaved about its placement?

like image 743
Alex Z Avatar asked Sep 16 '14 16:09

Alex Z


1 Answers

Considering that the examples in the documentation suffer from the same illness suggests that it won't be painless to solve this. It would seem that you have to live with the automatic ones, use manual placement, or get your hands dirty.

As a compromise, I'd try one of two things. Both start with letting matplotlib suggest label positions for you, then handling those which are too close to an axes.

The simpler case, which is also safer, is to just get rid of those clabels which are close to a border, filling those contour lines:

# based on matplotlib.pyplot.clabel example:
import matplotlib
import numpy as np
import matplotlib.cm as cm
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt

delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
# difference of Gaussians
Z = 10.0 * (Z2 - Z1)


plt.figure()
CS = plt.contour(X, Y, Z)
CLS = plt.clabel(CS, inline=1, fontsize=10)

# now CLS is a list of the labels, we have to find offending ones
thresh = 0.05  # ratio in x/y range in border to discard

# get limits if they're automatic
xmin,xmax,ymin,ymax = plt.axis()
Dx = xmax-xmin
Dy = ymax-ymin

# check which labels are near a border
keep_labels = []
for label in CLS:
    lx,ly = label.get_position()
    if xmin+thresh*Dx<lx<xmax-thresh*Dx and ymin+thresh*Dy<ly<ymax-thresh*Dy:
        # inlier, redraw it later
        keep_labels.append((lx,ly))

# delete the original lines, redraw manually the labels we want to keep
# this will leave unlabelled full contour lines instead of overlapping labels

for cline in CS.collections:
    cline.remove()
for label in CLS:
    label.remove()

CS = plt.contour(X, Y, Z)
CLS = plt.clabel(CS, inline=1, fontsize=10, manual=keep_labels)

The downside is that some labels will obviously be missing, and of course the 5% threshold should need manual tweaking for your specific application. Result of the above compared to the original (watch the top):

before after

The other solution I mentioned would be to take the offending labels, look at the Paths of their respective CS.collections data, and try to find a point which is closer to the insides of the figure. Since it's not trivial to pair the collections data with the labels (since each contour level path with its multiple segments corresponds to a single element of CS.collections), it might not all be worth the effort. Especially that you could be facing level lines so short that it's impossible to place a label on them, and you'd also have to estimate the size of each label as well.


Considering that in your case the contour lines are fairly simple, you could also try looking at each contour line, and finding that point which is closest to the center of the figure.

So, here's a reconstruction of your data set for demonstration purposes:

# guesstimated dummy data
X,Y = np.meshgrid(np.logspace(-3,7,200),np.logspace(13,31,200))
Z = X/Y*10**21
Vrange = range(-3,5)
V = [10**k for k in Vrange]
fmt = {lev: '$10^{%d}$'%k for (k,lev) in zip(Vrange,V)}


fig = plt.figure(figsize=(3,3))
ax = fig.add_subplot(111)
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel(r'$T_e$ (eV)', fontsize=10)
ax.set_ylabel(r'$n_e$ (1/cm$^3$)', fontsize=10)
ax.set_xlim(0.1, 1e4)
ax.set_ylim(1e16, 1e28)

CS = ax.contour(X, Y, Z, V, colors='k')
ax.clabel(CS, inline=True, inline_spacing=3, rightside_up=True, colors='k', fontsize=8, fmt=fmt)

By making explicit use of that both of your axes are logarithmic, the main idea is to replace the last call above to clabel with:

# get limits if they're automatic
xmin,xmax,ymin,ymax = plt.axis()
# work with logarithms for loglog scale
# middle of the figure:
logmid = (np.log10(xmin)+np.log10(xmax))/2, (np.log10(ymin)+np.log10(ymax))/2

label_pos = []
for line in CS.collections:
    for path in line.get_paths():
        logvert = np.log10(path.vertices)

        # find closest point
        logdist = np.linalg.norm(logvert-logmid, ord=2, axis=1)
        min_ind = np.argmin(logdist)
        label_pos.append(10**logvert[min_ind,:])

# draw labels, hope for the best
ax.clabel(CS, inline=True, inline_spacing=3, rightside_up=True, colors='k', fontsize=8, fmt=fmt, manual=label_pos)

Result (second) compared to the original (first):

before 2 after 2

I didn't make much of an effort to make the axes annotations pretty, so please ignore these details. You can see that the labels are indeed nicely gathered near the middle of the figure. Depending on your application, this might or might not be what you want.

As a final note, the reason the labels are not placed along the diagonal of the axes is that the scaling is different along the X and Y axes. This could cause some of the labels to reach out of the axes still. The most foolproof solution would be to consider the [xmin,ymax]--[xmax,ymin] (logarithmic) line, and to find the intersection of this line with each of the paths. You have to be very invested in this if this is worth it: you might as well place your labels fully manually.

like image 141