Reputation: 269
I´ve found many examples on how to avoid overlaping text on matplotlib but none that I could figure out how to apply to my case.
I have a dataframe (mapadf1) with some information about brazilian municipalities and i ploted over a shapefile of São Paulo's state (sp).
I created a variable "l", that containg the name of the municipality and the number I want to highlight. When the number is 0, the string is blank.
Ok so I managed to plot my map with the following code:
# set the range for the choropleth values
vmin, vmax = 0, 1
# create figure and axes for Matplotlib
fig, ax = plt.subplots(1, figsize=(30, 10))
# remove the axis que mostra latitude e longitude
ax.axis('off')
# add a title and annotation
ax.set_title('Número leitos inaugurados: 22/03', fontdict={'fontsize': '25', 'fontweight' : '3'})
ax.annotate('Fonte: Governo do Estado de São Paulo', xy=(0.6, .05), xycoords='figure fraction', fontsize=12, color='#555555')
# empty array for the data range
sm.set_array([]) # or alternatively sm._A = []. Not sure why this step is necessary, but many recommends it
# create map
mapa_df1.plot(column='tem_leito',cmap='Paired', linewidth=0.8, ax=ax, edgecolor='0.8')
# Add Labels
mapa_df1['coords'] = mapa_df1['geometry'].apply(lambda x: x.representative_point().coords[:])
mapa_df1['coords'] = [coords[0] for coords in mapa_df1['coords']]
for idx, row in mapa_df1.iterrows():
plt.annotate(s=row['l'], xy=row['coords'])
And my map:
How can I avoid the overlaping text?!
Thanks in advance!
Upvotes: 3
Views: 1524
Reputation: 110166
As it is, the object created by a plt.annotate
call is a matplotlib "annotation" - which has a lot of methods - and a bounding box, which can be retrieved by calling the .get_window_extent()
on the returning object.
If you don't have tens of thousands of points - which would not fit in a plot of this kind anyway, you can just store these coordinates in a list - and check for collisions linearly when adding another object. (For a few thousands objects this becomes infeasible, and a better than linear strategy would have to be used).
Now there is another problem: what to do if a collision do happen? he easier solution is simply not to display the offending label - but you can try to re-position the new annotation slightly so that it does not overlap. Doing that can be complex - but if we pick a simple naive strategy, of, say, just move the element on the y axis until it does not overlap anymore, you can get a nice result for a fairly sparse map, even if with some incorrections.
A "smarter" strategy could gather all nearby labels, and try to reposition then together in a tight way - that would require a couple hours or even days of work.
So, since you don't have an example with data we can reproduce locally, I will write the "move later annotations down on y axis until it fits" strategy." At least you will get a starting point.
from matplotlib.transforms import Bbox
...
text_rectangles = []
y_step = 0.05
# This will have far better results if the labels are sorted descending in the y axis -
#
mapa_df1["sort_key"] = [coord[1] for coord in mapa_df1["coords"]]
mapa_df1.sort_values("sort_key", ascending=False, inplace=True)
del mapa_df1["sort_key"]
for idx, row in mapa_df1.iterrows():
text = plt.annotate(s=row['l'], xy=row['coords'])
rect = text.get_window_extent()
for other_rect in text_rectangles():
while bbox.intersection(rect, other_rect): # overlapping
x, y = text.get_position()
text.set_position(x, y - y_step)
rect = text.get_window_extent()
text_rectangles.append(rect)
After that you can get hold of one of the Annotation instances created interactively, and explore its methods and properties - it is even possible to have then interact with the Pointer, depending on the rendering backend, so for example, labels could be drawn with a level of transparency, and become full opaque when the mouse pointer hovers then (see Possible to make labels appear when hovering over a point in matplotlib? , for example).
Upvotes: 1