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:
