◐ Shell
clean mode source ↗

Multiple choropleth maps

def create_gradient_colormap(colors):
    """
    Based on 2 color input, create a colormap of it.
    """
    cmap = LinearSegmentedColormap.from_list("custom_gradient", colors, N=256)
    return cmap

def draw_arrow(tail_position, head_position, fig, invert=False):
    """
    Draw a curve arrow at given tail/head position, on a figure.
    """
    kw = dict(
        arrowstyle="Simple, tail_width=0.5, head_width=4, head_length=8", color="white")
    if invert:
        connectionstyle = "arc3,rad=-.5"
    else:
        connectionstyle = "arc3,rad=.5"
    a = FancyArrowPatch(tail_position, head_position,
                        connectionstyle=connectionstyle,
                        transform=fig.transFigure,
                        **kw)
    fig.patches.append(a)

def plot_map_on_ax(column, ax, cmap):
    """
    Add a map on a given axis.
    """
    data.plot(
        column=column,
        cmap=cmap,
        edgecolor='black', linewidth=0.4,
        ax=ax
    )
    ax.set_xlim(-13.8, 40)
    ax.set_ylim(32, 72)
    ax.axis('off')

def path_effect_stroke(**kwargs):
    return [path_effects.Stroke(**kwargs), path_effects.Normal()]
pe = path_effect_stroke(linewidth=1, foreground="black")

# colors for the chart
colors = {
    'share_Generosity': ['#c9ada7', '#4a4e69'],
    'share_Perceptions of corruption': ['#fcbf49', '#d62828'],
    'share_Freedom to make life choices': ['#90e0ef', '#0077b6'],
    'share_Social support': ['#80ed99', '#38a3a5'],
}
background_col = '#22333b'

# initialize the figure
fig, axs = plt.subplots(nrows=3, ncols=2, figsize=(8, 10))
axs = axs.flatten()

# set background color
fig.set_facecolor(background_col)
axs[1].set_facecolor(background_col)

# list that we use to display maps,
# with empty values for 2 first axes
columns = [
    '', '',
    'share_Generosity',
    'share_Perceptions of corruption',
    'share_Freedom to make life choices',
    'share_Social support'
]

# annotation positions on the lollipop
annotations_pos = [
    '', '',
    [8, 0],
    [9, 1],
    [15, 2],
    [3, 3]
]

# iterate over of the 6 axes AND column names
for i, (ax, column) in enumerate(zip(axs, columns)):

    # skip first two axes (on top of the maps)
    if i in [0, 1]:
        continue

    # create a colormap based on colors
    cmap = create_gradient_colormap(colors[column])

    # add map on the current axe
    plot_map_on_ax(column=column, ax=ax, cmap=cmap)

    # annotations below each map
    ax_text(
        -15, 33, # fixed position for each map
        '<'+column[6:]+'>',
        ha='left', va='center',
        fontsize=9, fontweight='bold',
        color=cmap(0.5),
        highlight_textprops=[
            {"path_effects": pe}
        ], ax=ax
    )

    # annotations on lollipop
    x, y = annotations_pos[i]
    ax_text(
        x, y,
        '<'+column[6:]+'>',
        ha='left', va='center',
        fontweight='bold',
        fontsize=10,
        color=cmap(0.5),
        highlight_textprops=[
            {"path_effects": pe}
        ], ax=axs[1]
    )

# Lollipop plot
min_max_df = data[columns[2:]].agg(['min', 'max']).T
for i, col in enumerate(columns[2:]):

    # colors
    min_color = colors[col][0]
    max_color = colors[col][1]

    # filter on current column
    subset = min_max_df.iloc[i].T

    # add data points of lollipop
    axs[1].scatter(subset['min'], i, zorder=2, s=160, edgecolor='black', linewidth=0.5, color=min_color)
    axs[1].scatter(subset['max'], i, zorder=2, s=160, edgecolor='black', linewidth=0.5, color=max_color)

# horizontal lines of lollipop
axs[1].hlines(
    y=range(4),
    xmin=min_max_df['min'], xmax=min_max_df['max'],
    color='white', linewidth=0.8, zorder=1
)

# custom lollipop axis features
axs[1].spines[['right', 'top', 'left']].set_visible(False)
axs[1].set_xticks([0, 10, 20, 30, 40])
axs[1].spines['bottom'].set_color('white')
axs[1].tick_params(axis='x', colors='white')
axs[1].set_yticks([])
axs[1].set_ylim(-1, 6)
axs[1].set_xlim(-3, 33)

# remove top left axis
axs[0].set_axis_off()

# title and credit
text = """
<What determines happiness>
<in Europe? Well, it depends>


<Share, in %, of happiness explained by different>
<factors across Europe. For each country, the>
<darker the color is, the more the factor explains>
<happiness in this country.>
"""
ax_text(
    -0.02, 0.6,
    text,
    ha='left', va='center',
    fontsize=15,
    color='black',
    highlight_textprops=[
        {'fontweight': 'bold',
         'color': 'white'},
        {'fontweight': 'bold',
         'color': 'white'},

        {'color': 'darkgrey',
         'fontsize': 11},
        {'color': 'darkgrey',
         'fontsize': 11},
        {'color': 'darkgrey',
         'fontsize': 11},
        {'color': 'darkgrey',
         'fontsize': 11}
    ],
    ax=axs[0]
)

# credit
text = """
<Design:> Joseph Barbier
<Data:> World Happiness Report 2024
"""
ax_text(
    -17.6, 25, text,
    ha='left', va='center',
    fontsize=6, color='white',
    highlight_textprops=[
        {'fontweight': 'bold'},
        {'fontweight': 'bold'},
    ], ax=axs[4]
)

# reduce size and change position of lollipop axe
axs[1].set_position([0.56, 0.68, 0.2, 0.1])

# legend arrows
draw_arrow((0.7, 0.92), (0.76, 0.87), fig=fig, invert=True)
draw_arrow((0.82, 0.92), (0.862, 0.87), fig=fig, invert=True)
ax_text(
    6, 4.7, 'Minimum',
    fontsize=8, color='white',
    ax=axs[1]
)
ax_text(
    16.7, 4.8, 'Maximum',
    fontsize=8, color='white',
    ax=axs[1]
)

plt.tight_layout()
fig.savefig('../../static/graph/web-multiple-maps.png', dpi=300, bbox_inches='tight')
plt.show()