# Lineplot and Stacked area chart in Matplotlib

A custom vizualisation that combines a lineplot with a stacked area chart to explore the evolution of child labour made with `Python` and `Matplotlib`. This blogpost guides you through a step-by-step construction of every aspect of the plot, including a variety of custom color annotations, labels, and more!

Thanks to them for all the inspiring and insightful visualizations! Thanks also to Tomás Capretto who replicated the chart in Python! 🙏🙏

As a teaser, here is the plot we’re gonna try building:

At first sight, one may be tempted to think that today's chart looks rather simple. However, it actually contains several subtle customizations that when added all together make the final result look beautiful. This is also going to be a great opportunity to try an interesting variety of tools from Matplotlib.

This post also uses the `flexitext()` function from the `flexitext` library. It is going to be tremendously helpful when drawing titles that mix both regular and bold text.

``````import numpy as np
import matplotlib.pyplot as plt

from flexitext import flexitext

from matplotlib import lines
from matplotlib import patches
from matplotlib.patheffects import withStroke``````

Let's define colors that are going to be used througout this blogpost:

``````BROWN = "#AD8C97"
BROWN_DARKER = "#7d3a46"
GREEN = "#2FC1D3"
BLUE = "#076FA1"
GREY = "#C7C9CB"
GREY_DARKER = "#5C5B5D"
RED = "#E3120B"``````

## Linechart

The chart we're going to reproduce today is made of two separated plots, a linechart and a stacked area chart. We'll do the linechart first.

First of all, let's get started by creating the objects that are going to hold the data for us. Note these values are inferred from the original plot and not something computed from the original data source.

``````year = [2008, 2012, 2016, 2020]

latin_america = [10, 9, 7.5, 5.8]
asia_and_pacific = [13.5, 9.5, 7.5, 5.5]
sub_saharan_africa = [25.5, 21, 22.2, 24]
percentages = [sub_saharan_africa, asia_and_pacific, latin_america]

COLORS = [BLUE, GREEN, BROWN]``````

### Basic linechart

``````# Initialize plot ------------------------------------------
fig, ax = plt.subplots(figsize=(8, 6))

# Note the zorder to have dots be on top of the lines
for percentage, color in zip(percentages, COLORS):
ax.plot(year, percentage, color=color, lw=5)
ax.scatter(year, percentage, fc=color, s=100, lw=1.5, ec="white", zorder=12)``````

This is a fair start! There's still lot to do! Let's continue with some axis customizations.

### Customize axis

``````# Customize axis -------------------------------------------
# Customize y-axis ticks
ax.yaxis.set_ticks([i * 5 for i in range(0, 7)])
ax.yaxis.set_ticklabels([i * 5 for i in range(0, 7)])
ax.yaxis.set_tick_params(labelleft=False, length=0)

# Customize y-axis ticks
ax.xaxis.set_ticks([2008, 2012, 2016, 2020])
ax.xaxis.set_ticklabels([2008, 12, 16, 20], fontsize=16, fontfamily="Econ Sans Cnd", fontweight=100)
ax.xaxis.set_tick_params(length=6, width=1.2)

# Make gridlines be below most artists.
ax.set_axisbelow(True)

ax.grid(axis = "y", color="#A8BAC4", lw=1.2)

# Remove all spines but the one in the bottom
ax.spines["right"].set_visible(False)
ax.spines["top"].set_visible(False)
ax.spines["left"].set_visible(False)

# Customize bottom spine
ax.spines["bottom"].set_lw(1.2)
ax.spines["bottom"].set_capstyle("butt")

# Set custom limits
ax.set_ylim(0, 35)
ax.set_xlim(2007.5, 2021.5)

fig``````

It starts to look elegant!

This is where one can see a very subtle detail in action. If you have a look at the label for Lation America and the Caribbean, you are going to notice the text does not overlap with the horizontal grid line at 5, as if the text has a background or a border. In the following chunk, we're going to create this effect using the `withStroke()` path effect in Matplotlib. This is going to add a border to te text that is going to cover the grid line passing behind the text.

``````# Add labels for vertical grid lines -----------------------
# The pad is equal to 1% of the vertical range (35 - 0)
for label in [i * 5 for i in range(0, 7)]:
ax.text(
ha="right", va="baseline", fontsize=18,
fontfamily="Econ Sans Cnd", fontweight=100
)

# Annotate labels for regions ------------------------------

# Note the path effect must be a list
path_effects = [withStroke(linewidth=10, foreground="white")]

# We create a function to avoid repeating 'ax.text' many times
def add_region_label(x, y, text, color, path_effects, ax):
ax.text(
x, y, text, color=color,
fontfamily="Econ Sans Cnd", fontsize=18,
va="center", ha="left", path_effects=path_effects
)
region_labels = [
{
"x": 2007.9, "y": 5.8, "text": "Latin America and\nthe Caribbean",
"color": BROWN_DARKER, "path_effects": path_effects},
{
"x": 2010, "y": 13, "text": "Asia and the Pacific",
"color": GREEN, "path_effects": []
},
{
"x": 2007.9, "y": 27, "text": "Sub-Saharan Africa",
"color": BLUE, "path_effects": []
},
]

for label in region_labels:

fig``````

Do you see that the grid line at five cannot be seen between the letters in the Caribbean? This is because of the effect we've just added.

The last step to reproduce this plot is to add a proper title. Note this title mixes bold and regular text, and also contains a little horizontal line on top of it.

Matplotlib does not provide any function to mix both normal and bold text. Fortunately, there's `flexitext`. This allows us to draw text with different formats very easily using a formatted string.

``````# Add title ------------------------------------------------

# Use flexitext instead of `ax.text()`
text = "<name:Econ Sans Cnd, size:18><weight:bold>Selected regions,</> % of child population</>"
flexitext(0, 0.975, text, va="top", ax=ax)

# This is the small line on top of the title
# Note the 'solid_capstyle' and the 'transform', these are very important.
lines.Line2D(
[0, 0.05], [1, 1], lw=2, color="black",
solid_capstyle="butt", transform=ax.transAxes
)
)
fig``````

## Stacked area chart

The stacked area chart on the right contains information about the rest of the world too, so we add the grey color to the `COLORS` list. This is also where the data for the counts is created.

``````COLORS += [GREY]
counts = [
[65, 55, 67, 85],
[130, 85, 65, 50],
[10, 10, 10, 8],
[60, 20, 10, 16]

]``````

### Basic stacked area chart

Thanks to the `.stackplot()` method, it is quite straightforward to create a stacked area chart in Matplotlib. The `lw` and the `edgecolor` arguments correspond to the linewidth and the color of the line between the areas.

``````# Initialize plot ------------------------------------------
fig, ax = plt.subplots(figsize=(8, 6))

ax.stackplot(year, counts, colors=COLORS, lw=1.5, edgecolor='white');``````

### Customize axis

As with the linechart, the second step is to customize the axis.

``````# Customize y-axis ticks
ax.yaxis.set_ticks([i * 50 for i in range(0, 7)])
ax.yaxis.set_ticklabels([i * 50 for i in range(0, 7)])
ax.yaxis.set_tick_params(labelleft=False, length=0)

# Customize x-axis ticks
ax.xaxis.set_ticks([2008, 2012, 2016, 2020])
ax.xaxis.set_ticklabels([2008, 12, 16, 20], fontsize=16, fontfamily="Econ Sans Cnd", fontweight=100)
ax.xaxis.set_tick_params(length=6, width=1.2)

# Make gridlines be below most artists.
ax.set_axisbelow(True)

ax.grid(axis = "y", color="#A8BAC4", lw=1.2)

# Remove all spines but the one in the bottom
ax.spines["right"].set_visible(False)
ax.spines["top"].set_visible(False)
ax.spines["left"].set_visible(False)

# Customize bottom spine
ax.spines["bottom"].set_lw(1.2)
ax.spines["bottom"].set_capstyle("butt")

# Specify both horizontal and vertical limits
ax.set_ylim(0, 350)
ax.set_xlim(2007.5, 2021.5)

fig``````

Now it's the turn for labels and annotations. Notice the `path_effects` are empty now because we don't need to add any border effect. On the other hand, do also notice how we come up with something that looks like an arrow with a circle in the tip.

``````# Add labels for vertical grid lines -----------------------
# The pad is equal to 1% of the vertical range (350 - 0)
for label in [i * 50 for i in range(0, 7)]:
ax.text(
ha="right", va="baseline", fontsize=18,
fontfamily="Econ Sans Cnd", fontweight=100
)

# Annotate labels for regions ------------------------------
# We use the 'add_region_labels()' function from above
region_labels = [
{"x": 2013, "y": 225, "text": "Latin America and\nthe Caribbean", "color": BROWN_DARKER, "path_effects":[]},
{"x": 2013, "y": 100, "text": "Asia and the Pacific", "color": "white", "path_effects":[]},
{"x": 2013, "y": 25, "text": "Sub-Saharan Africa", "color": "white", "path_effects":[]},
{"x": 2008.05, "y": 225, "text": "Rest\nof world", "color": GREY_DARKER, "path_effects":[]},
]

for label in region_labels:

# Add custom arrow-like line -------------------------------
# It's not possible to use a dot as an arrowhead.
# using `ax.scatter()` as shown below
patches.FancyArrowPatch(
(2016.25, 214), (2018.5, 137),
arrowstyle = "Simple",
color="k"
)
)

ax.scatter(2018.5, 138, s=10, color="k")

fig``````

And finally, just add the title. There's nothing new here, since it uses the same techniques than the other chart.

``````# Add title ------------------------------------------------

# Use flexitext instead of `ax.text()`
text = "<name:Econ Sans Cnd, size:18><weight:bold>Number of children,</> m</>"
flexitext(0, 0.975, text, va="top", ax=ax)

# Same line on top of title
lines.Line2D(
[0, 0.05], [1, 1], lw=2, color="black",
solid_capstyle="butt", transform=ax.transAxes
)
)

fig``````

## Full chart

Let's get started by creating a layout with two subplots. This is also adjusted so it does not contain any extra space on both left and right ends.

``````fig, axes = plt.subplots(1, 2, figsize=(12, 7.2))

# Set background to white. Useful when saving a .png
fig.set_facecolor("w")``````

You may have noticed that many of the steps to customize the axis are quite repetitive. The following function takes an `Axis` object and apply several customizations that are common to both the left and right plots.

``````def customize_axis(ax):
# Make gridlines be below most artists.
ax.set_axisbelow(True)

ax.grid(axis = "y", color="#A8BAC4", lw=1.2)

# Customize x-axis ticks
ax.xaxis.set_ticks([2008, 2012, 2016, 2020])
ax.xaxis.set_ticklabels([2008, 12, 16, 20], fontsize=16, fontfamily="Econ Sans Cnd", fontweight=100)
ax.xaxis.set_tick_params(length=6, width=1.2)

# Remove all spines but the one in the bottom
ax.spines["right"].set_visible(False)
ax.spines["top"].set_visible(False)
ax.spines["left"].set_visible(False)

# Customize bottom spine
ax.spines["bottom"].set_lw(1.2)
ax.spines["bottom"].set_capstyle("butt")
``````

In this step, we add the linechart to the layout created above. Note the code is exactly like the code we used to create the plot above. Most of the comments have been removed to avoid more redundancy.

``````# Add lines with dots
for percentage, color in zip(percentages, COLORS):
axes[0].plot(year, percentage, color=color, lw=5)
axes[0].scatter(year, percentage, fc=color, s=100, lw=1.5, ec="white", zorder=12)

# Customize axis -------------------------------------------
axes[0].yaxis.set_ticks([i * 5 for i in range(0, 7)])
axes[0].yaxis.set_ticklabels([i * 5 for i in range(0, 7)])
axes[0].yaxis.set_tick_params(labelleft=False, length=0)

customize_axis(axes[0])

axes[0].set_ylim(0, 35)
axes[0].set_xlim(2007.5, 2021.5)

# Add labels for vertical grid lines -----------------------
for label in [i * 5 for i in range(0, 7)]:
axes[0].text(
ha="right", va="baseline", fontsize=18,
fontfamily="Econ Sans Cnd", fontweight=100
)

# Annotate labels for regions ------------------------------
path_effects = [withStroke(linewidth=10, foreground="white")]
region_labels = [
{
"x": 2007.9, "y": 5.8, "text": "Latin America and\nthe Caribbean",
"color": BROWN_DARKER, "path_effects": path_effects},
{
"x": 2010, "y": 13, "text": "Asia and the Pacific",
"color": GREEN, "path_effects": []
},
{
"x": 2007.9, "y": 27, "text": "Sub-Saharan Africa",
"color": BLUE, "path_effects": []
},
]

for label in region_labels:

# Use flexitext instead of `ax.text()`
text = "<name:Econ Sans Cnd, size:18><weight:bold>Selected regions,</> % of child population</>"
flexitext(0, 0.975, text, va="top", ax=axes[0])
lines.Line2D(
[0, 0.05], [1, 1], lw=2, color="black",
solid_capstyle="butt", transform=axes[0].transAxes
)
)
fig``````

Similarily than with the linechart, this adds the stacked area chart to the layout.

``````# Add stacked area
axes[1].stackplot(year, counts, colors=COLORS, lw=1.5, edgecolor='white');

# Customize axis -------------------------------------------
axes[1].yaxis.set_ticks([i * 50 for i in range(0, 7)])
axes[1].yaxis.set_ticklabels([i * 50 for i in range(0, 7)])
axes[1].yaxis.set_tick_params(labelleft=False, length=0)

customize_axis(axes[1])

axes[1].set_ylim(0, 350)
axes[1].set_xlim(2007.5, 2021.5)

# Add labels for vertical grid lines -----------------------
for label in [i * 50 for i in range(0, 7)]:
axes[1].text(
ha="right", va="baseline", fontsize=18,
fontfamily="Econ Sans Cnd", fontweight=100
)

# Annotate labels for regions ------------------------------
region_labels = [
{"x": 2013, "y": 225, "text": "Latin America and\nthe Caribbean", "color": BROWN_DARKER, "path_effects":[]},
{"x": 2013, "y": 100, "text": "Asia and the Pacific", "color": "white", "path_effects":[]},
{"x": 2013, "y": 25, "text": "Sub-Saharan Africa", "color": "white", "path_effects":[]},
{"x": 2008.05, "y": 225, "text": "Rest\nof world", "color": GREY_DARKER, "path_effects":[]},
]

for label in region_labels:

# Add custom arrow-like line -------------------------------
patches.FancyArrowPatch(
(2016.8, 215), (2019.4, 137),
arrowstyle = "Simple",
color="k"
)
)

axes[1].scatter(2019.4, 138, s=10, color="k")

text = "<name:Econ Sans Cnd, size:18><weight:bold>Number of children,</> m</>"
flexitext(0, 0.975, text, va="top", ax=axes[1])

lines.Line2D(
[0, 0.05], [1, 1], lw=2, color="black",
solid_capstyle="butt", transform=axes[1].transAxes
)
)

fig``````

Finally, it starts to look like the original chart on top. It's been a lot of work, we're really close to get the final result. Just the next step, and this will be done. Let's do it!

Subtle, well-thought annotations and marks, together with a style refined throughout the years, are what make visualizations from The Economist stand out. In this last step, we add extra annotations that are going to give this chart the final tweaks it needs.

``````# Make room below on top and bottom

fig.text(
0, 0.92, "All work, no play",
fontsize=22,
fontweight="bold",
fontfamily="Econ Sans Cnd"
)
fig.text(
0, 0.875, "Children in child labour*",
fontsize=20,
fontfamily="Econ Sans Cnd"
)

source = 'Source: "Child Labour: Global estimates 2020, trends and the road forward", ILO and UNICEF'
fig.text(
0, 0.06, source, color="#a2a2a2",
fontsize=14, fontfamily="Econ Sans Cnd"
)
fig.text(
1, 0.06, "*5- to 17- year-olds", color="#a2a2a2", ha="right",
fontsize=14, fontfamily="Econ Sans Cnd"
)
fig.text(
0, 0.005, "The Economist", color="#a2a2a2",
fontsize=16, fontfamily="Milo TE W01"
)

# Add line and rectangle on top.
fig.add_artist(lines.Line2D([0, 1], [1, 1], lw=3, color=RED, solid_capstyle="butt"))
fig

# If you want to save the plot to see it in better quality
#fig.savefig("plot.png", dpi=300)``````

Voilà! We nailed it! 🎉

Line chart

Area chart

Stacked Area

Streamgraph

Timeseries

## Contact & Edit

👋 This document is a work by Yan Holtz. Any feedback is highly encouraged. You can fill an issue on Github, drop me a message onTwitter, or send an email pasting `yan.holtz.data` with `gmail.com`.

Violin

Density

Histogram

Boxplot

Ridgeline

Scatterplot

Heatmap

Correlogram

Bubble

Connected Scatter

2D Density

Barplot

Wordcloud

Parallel

Lollipop

Circular Barplot

Treemap

Venn Diagram

Donut

Pie Chart

Dendrogram

Circular Packing

Line chart

Area chart

Stacked Area

Streamgraph

Timeseries

Map

Choropleth

Hexbin

Cartogram

Connection

Bubble

Chord Diagram

Network

Sankey

Arc Diagram

Edge Bundling

Colors

Interactivity

Animation

Cheat sheets

Caveats

3D