0

I made the choropleth map using GeoPandas and Matplotlib. I want to add value labels to each polygon of the map in a way that font label color must be a contrast to polygon fill color (white on a darker color and black on a lighter).

Thus, I need to know every polygon's fill color. I found the solution (see minimal working example code below).

But I suppose that a more simple and clear solution exists, so I post this question with the hope to find it with community help.

import geopandas as gpd
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from itertools import islice, pairwise
from matplotlib.collections import PatchCollection

def contrast_color(color):
    d = 0
    r, g, b = (round(x*255, 0) for x in color[:3])     
    luminance = 1 - (0.299 * r + 0.587 * g + 0.114 * b) / 255
    d = 0 if luminance < 0.5 else 255
    return (d, d, d)

def get_colors(ax):
    # get childrens 
    # to obtain a PatchCollection
    _ = ax.axes.get_children()
    collection = _[0]  # suppose it is first element
    if not isinstance(collection, PatchCollection):
        raise TypeError("This is not Collection")
    # get information about polygons fill colors
    # .get_facecolors() returns ALL colors for ALL polygons
    # that belongs to one multipolygon
    # e. g. if we have two multipolygons, 
    # and the first consists of two polygons 
    # and second consists of one polygon
    # we obtain THREE colors
    poly_colors = collection.get_facecolors()
    return poly_colors.tolist()

gpd.read_file("https://gist.githubusercontent.com/ap-Codkelden/72f988e2bcc90ea3c6c9d6d989d8eb3b/raw/c91927bdb6b199c4dd6df6759200a5a1e4b820f0/obl_sample.geojson")
dfm['coords'] = [x[0] for x in dfm['geometry'].apply(lambda x: x.representative_point().coords[:])]

fig, ax = plt.subplots(1, figsize=(10, 6))
ax.axis('off')
ax.set_title('Title', fontdict={'fontsize': '12', 'fontweight' : '3'})
dfm.plot(
    ax=ax,
    column='Average', cmap='Blues_r',
    linewidth=0.5, edgecolor='k',
    scheme='FisherJenks', k=2,
    legend=True
)

out = []  # empty container for colors
# count polygons for every multipolygon
# since it can contains more than one
poly_count = dfm.geometry.apply(lambda x: len(x.geoms)).to_list()
poly_colors = get_colors(ax)
# we need split the polygon's colors list into sublists, 
# where every sublist will contain all colors for 
# every polygon that belongs to one multipolygon
slices = [(0, poly_count[0])] + [x for x in pairwise(np.cumsum(poly_count))]
# splitting
for s in slices:
    out.append(
        set(tuple(x) for x in islice(poly_colors, *s)),)
# remove transparensy info
out = [next(iter(x))[:3] for x in out]
dfm['color'] = [tuple([y/255 for y in x]) for x in map(contrast_color, out)]

for idx, row in dfm.iterrows():
    plt.annotate(
        f"{row['reg_en']}\n{row['Average']:.2f}",
        xy=row['coords'], horizontalalignment='center',
        color=row['color'], size=9)

Desired labels are:

codkelden
  • 332
  • 6
  • 9
  • The essence of this question is to make all annotation texts readable everywhere on the map, right? If so, cartographic techniques can be used. For example, draw texts in 2 layers , thicker white texts on lower layer, and normal black texts on upper layer. Use zorder to arrange layers, higher is on top of lower values. – swatchai Jan 04 '23 at 03:49
  • @swatchai Yes, you're right -- the essence is in text visibility. Thank you for your suggested solution, I didn't think about the cartographic approach at all. But along with that purely applied interest still remain: is achievable to know a polygon's color by some simplified technique? – codkelden Jan 07 '23 at 21:25
  • 1
    Here are some examples worth considering https://matplotlib.org/stable/tutorials/advanced/patheffects_guide.html – swatchai Jan 08 '23 at 03:30

0 Answers0