Funnel charts with Python. A great option for representing... | by Thiago Carvalho | Towards Data Science

Funnel charts are mostly used for representing a sequential process, allowing the viewers to compare and see how the numbers change through the stages.
In this article, we’ll explore how to build a funnel chart from scratch using Matplotlib, and then we’ll have a look at an easier implementation with Plotly.
There is no method for instantly creating funnel charts in Matplotlib, so let’s start with a simple horizontal bar chart and build from there.
import matplotlib.pyplot as plty = [5,4,3,2,1]x = [80,73,58,42,23]plt.barh(y, x)
That’s actually quite close to what we want, so why not stop here and use a bar chart instead?
It may be even easier to compare the values with a simple bar chart, but choosing the funnel-shaped one can make the relationship between the bars more explicit and make our visualization more appealing.
Ok, so we’ll need to plot one bar at a time and use the ‘left’ parameter to adjust its position in the chart. Let’s check how this works.
y = [5,4,3,2,1]x = [80,73,58,42,23]x_max = 100x_min = 0for idx, val in enumerate(x):    plt.barh(y[idx], x[idx], left = idx+5)plt.xlim(x_min, x_max)
We have the bars' size, which is x and the range of the x-axis, which is 100.
The difference between those values is the blank space, and to center the bars, we need to have the same amount of blank space on each side.
So we can say that:left = (size of the bar - x-axis range) / 2
Let’s see how this looks.
y = [5,4,3,2,1]x = [80,73,58,42,23]x_max = 100x_min = 0for idx, val in enumerate(x):    left = (x_max - val)/2    plt.barh(y[idx], x[idx], left = left, color='grey')plt.xlim(x_min, x_max)
Great! But the axis information doesn’t mean much now that the bars don’t have the same starting point.
We need to print the values on the bars and connect them.
y = [5,4,3,2,1]x = [80,73,58,42,23]x_max = 100x_min = 0fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in enumerate(x):    left = (x_max - val)/2    plt.barh(y[idx], x[idx], left = left, color='grey', height=1, edgecolor='black')    # value    plt.text(50, y[idx], x[idx], ha='center', fontproperties=font,             fontsize=16, color='#2A2A2A')plt.axis('off')plt.xlim(x_min, x_max)
Alright, we could add a title, some labels for the bars, and call it a day.
But let’s give our viewers some eye-candy and add a shadow connecting the bars instead of just stacking them. That will make our chart and the connection between the bars more clear.
First, let’s define all the variables we’ll need
from matplotlib import font_manager as fm# funnel charty = [5,4,3,2,1]x = [80,73,58,42,23]labels = ['Hot Leads', 'Samples Sent', 'Quotes', 'Negotiations', 'Sales']x_max = 100x_min = 0x_range = x_max - x_minfpath = "fonts/NotoSans-Regular.ttf"font = fm.FontProperties(fname=fpath)
Now, let’s add some more details to our plot.
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in enumerate(x):    left = (x_range - val)/2    plt.barh(y[idx], x[idx], left = left, color='#808B96',             height=.8, edgecolor='black')    # label    plt.text(50, y[idx]+0.1, labels[idx], ha='center',              fontproperties=font, fontsize=16, color='#2A2A2A')    # value    plt.text(50, y[idx]-0.3, x[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')    plt.xlim(x_min, x_max)plt.axis('off')plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')plt.show()
*Font from https://fonts.google.com/specimen/Noto+Sans
We’ll plot a line from one side to the other of the bars. We can use the value of left to find the start and end of the bar and the y-value to find its center.
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in enumerate(x):    left = (x_range - val)/2    plt.barh(y[idx], x[idx], left = left, color='#808B96', height=.8, edgecolor='black')    # label    plt.text(50, y[idx]+0.1, labels[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')    # value    plt.text(50, y[idx]-0.3, x[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')        plt.plot([left, 100-left], [y[idx], y[idx]])plt.xlim(x_min, x_max)plt.axis('off')plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')plt.show()
Now let’s move this line to the bottom of the bar, and since we won’t have a connection after the last bar, we can skip drawing a shadow for it.
We defined the bars' height as 0.8, so to move the lines to the bottom, we could decrease y to 0.4.
We’ll also need the coordinates of the top of the next bar.
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in enumerate(x):    left = (x_range - val)/2    plt.barh(y[idx], x[idx], left = left, color='#808B96', height=.8, edgecolor='black')    # label    plt.text(50, y[idx]+0.1, labels[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')    # value    plt.text(50, y[idx]-0.3, x[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')        if idx != len(x)-1:        next_left = (x_range - x[idx+1])/2        plt.plot([left, 100-left],                  [y[idx]-0.4, y[idx]-0.4])        plt.plot([next_left, 100-next_left],                  [y[idx+1]+0.4, y[idx+1]+0.4])plt.xlim(x_min, x_max)plt.axis('off')plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')plt.show()
We found all the points we need to draw. Now we can connect those points and see if we get the shape we’re looking for.
Don’t forget to repeat the first point at the end to close the polygon.
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in enumerate(x):    left = (x_range - val)/2    plt.barh(y[idx], x[idx], left = left, color='#808B96', height=.8, edgecolor='black')    # label    plt.text(50, y[idx]+0.1, labels[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')    # value    plt.text(50, y[idx]-0.3, x[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')        if idx != len(x)-1:        next_left = (x_range - x[idx+1])/2        shadow_x = [left, next_left,                     100-next_left, 100-left, left]                shadow_y = [y[idx]-0.4, y[idx+1]+0.4,                     y[idx+1]+0.4, y[idx]-0.4, y[idx]-0.4]        plt.plot(shadow_x, shadow_y)plt.xlim(x_min, x_max)plt.axis('off')plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')plt.show()
Perfect. All that’s left to do is change the .plot to .fill and we have our funnel chart ready.
fig, ax = plt.subplots(1, figsize=(12,6))for idx, val in enumerate(x):    left = (x_range - val)/2    plt.barh(y[idx], x[idx], left = left,              color='#808B96', height=.8)    # label    plt.text(50, y[idx]+0.1, labels[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')    # value    plt.text(50, y[idx]-0.3, x[idx], ha='center',             fontproperties=font, fontsize=16, color='#2A2A2A')        if idx != len(x)-1:        next_left = (x_range - x[idx+1])/2        shadow_x = [left, next_left,                     100-next_left, 100-left, left]                shadow_y = [y[idx]-0.4, y[idx+1]+0.4,                     y[idx+1]+0.4, y[idx]-0.4, y[idx]-0.4]        plt.fill(shadow_x, shadow_y, color='grey', alpha=0.6)plt.xlim(x_min, x_max)plt.axis('off')plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')plt.show()
There it is!
Drawing a funnel chart with Matplotlib can go from simple to complex very quickly.
But that’s the fun of Matplotlib; we have the freedom to draw pretty much anything.
Now to an easier way of achieving the same results, let’s check how to draw a funnel chart with Plotly.
import plotly.express as pxdata = dict(values=[80,73,58,42,23],            labels=['Hot Leads', 'Samples Sent',                     'Quotes', 'Negotiations', 'Sales'])fig = px.funnel(data, y='labels', x='values')fig.show()
That’s awesome. Plotly has a method for drawing funnel charts, so we only need the data, and it does all the rest.
Plotly also offers many options to customize the charts.
We may not have so much control over the visualization as we did before, but it’s way more convenient to plot it like this.
data = dict(Quantity=[80, 73, 58, 42, 23,                      180, 120, 82, 51, 33,                      109, 78, 62, 44, 22],                Stage=['Hot Leads', 'Samples Sent', 'Quotes',                   'Negotiations', 'Sales']*3,                Location=['Tatooine']*5 + ['Mandalore']*5 + ['Nevarro']*5)    fig = px.funnel(data, y='Stage', x='Quantity', color='Location',                color_discrete_map={"Tatooine": "#374B53",                                     "Mandalore": "#617588",                                    "Nevarro": "#A4B7C8"},                template="simple_white",                title='Beskar Forging Services Inc.',                labels={"Stage": ""})fig.show()
And that’s it! We saw how to build a funnel chart from scratch with Matplotlib and how this simple visualization can get complicated. We also checked a more convenient way of drawing this chart with Plotly and got a peek at some of the ways we can customize it.
Thanks for reading my article. I hope you enjoyed it.
More tutorials on DataViz with Python.