jvqp
jvqp

Reputation: 269

Avoid overlaping labels at matplotlib + geopandas

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:

enter image description here

How can I avoid the overlaping text?!

Thanks in advance!

Upvotes: 3

Views: 1524

Answers (1)

jsbueno
jsbueno

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

Related Questions