Hello, I’m Chi Ye

As we all know, Python is widely used. Today we will learn how to make beautiful bullet diagrams using the Matplotlib library

1. What is a bullet map

A bullet diagram is a conventional definition

Bullet charts encode data using length/height, position, and color to show how it actually looks compared to targets and performance bands

Let’s take a look at what the bullet diagram looks like, okay

The bullet chart has a single primary metric (for example, current year-to-date revenue), compares that metric with one or more other measures to enrich its meaning (for example, compared to targets), and displays it in the context of qualitative ranges of performance, such as poor, satisfactory and good. Qualitative ranges are displayed as different intensifiers of a single hue, allowing color-blind people to distinguish them and keeping color use on the dashboard to a minimum

Ok, this is almost the bullet diagram application scenario and drawing standards, let’s start to make

2. Build charts

The idea is that you can use a stacked bar chart to represent ranges, another smaller bar chart to represent values, and finally, a vertical line to mark targets

As you can see, we need multiple component layers, which is easier to implement using Matplotlib

import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.ticker import FuncFormatter

%matplotlib inline
Copy the code

We also imported Seaborn here because Seaborn has some very useful tools for managing palettes, which is easier to take advantage of than trying to copy it in other ways

The main reason we need to generate color palettes is that we most likely want to generate visually appealing color schemes for various qualitative ranges, and it would be much easier to do this directly using Seaborn

In the example below, we can use the Palplot convenience function to display a palette of five green hues

sns.palplot(sns.light_palette("green", 5))
Copy the code

sns.palplot(sns.light_palette("purple",8, reverse=True))
Copy the code

Make 8 different shades of purple in reverse order

Now that we know how to set up the palette, let’s use Matplotlib to create a simple bullet diagram based on the principles listed above

First, define the values we want to draw

limits = [80, 100, 150]
data_to_plot = ("Example 1", 105, 120)
Copy the code

This will create 3 ranges: 0-80, 81-100, 101-150 and an “example” line with a value of 105 and a target line of 120. Next, build a blue palette:

palette = sns.color_palette("Blues_r", len(limits))
Copy the code

Here’s the stack bar chart for the build scope:

fig, ax = plt.subplots()
ax.set_aspect('equal')
ax.set_yticks([1])
ax.set_yticklabels([data_to_plot[0]])

prev_limit = 0
for idx, lim in enumerate(limits):
    ax.barh([1], lim-prev_limit, left=prev_limit, height=15, color=palette[idx])
    prev_limit = lim
Copy the code

Then we can add a smaller bar chart to represent the value of 105:

ax.barh([1], data_to_plot[1], color='black', height=5)
Copy the code

It’s starting to take shape

The final step is to add the target tag using axvline:

Ax. Axvline (data_to_plot [2], color = "gray", ymin = 0.10, ymax = 0.9)Copy the code

I’ve done a simple bullet diagram above, but all of our test values are written dead, so let’s write a code that can fill in any number

3. Final code

def bulletgraph(data=None, limits=None, labels=None, axis_label=None, title=None, size=(5, 3), palette=None, formatter=None, target_color="gray", bar_color="black", label_color="gray"): # Determine the max value for adjusting the bar height # Dividing by 10 seems to work pretty well h = limits[-1] / 10 # Use the green palette as a sensible default if palette is None: palette = sns.light_palette("green", len(limits), reverse=False) # Must be able to handle one or many data sets via multiple subplots if len(data) == 1: fig, ax = plt.subplots(figsize=size, sharex=True) else: fig, axarr = plt.subplots(len(data), figsize=size, sharex=True) # Add each bullet graph bar to a subplot for idx, item in enumerate(data): # Get the axis from the array of axes returned when the plot is created if len(data) > 1: ax = axarr[idx] # Formatting to get rid of extra marking clutter ax.set_aspect('equal') ax.set_yticklabels([item[0]]) ax.set_yticks([1]) ax.spines['bottom'].set_visible(False) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_visible(False) prev_limit = 0 for idx2, lim in enumerate(limits): # Draw the bar ax.barh([1], lim - prev_limit, left=prev_limit, height=h, color=palette[idx2]) prev_limit = lim rects = ax.patches # The last item in the list is the value we're measuring # Draw  the value we're measuring ax.barh([1], item[1], height=(h / 3), color=bar_color) # Need the ymin and max in order to make sure the target marker # fits ymin, Ymax = ax.get_ylim() ax.vlines(item[2], ymin *.9, ymax *.9, lineWidth =1.5, color=target_color) # Now make some labels if labels is not None: for rect, label in zip(rects, labels): height = rect.get_height() ax.text( rect.get_x() + rect.get_width() / 2, -height * .4, label, ha='center', va='bottom', color=label_color) if formatter: ax.xaxis.set_major_formatter(formatter) if axis_label: ax.set_xlabel(axis_label) if title: fig.suptitle(title, fontsize=14) fig.subplots_adjust(hspace=0)Copy the code

Although the code looks a bit long, it is actually a superposition of the above steps, which is relatively simple and will not be explained again

Let’s just call it and see what happens

Data_to_plot2 = [(" zhang ", 105, 120), (" bill ", 99, 110), (" detective ", 109, 125), (" Daisy ", 135, 123), (" seven "money, 45, 105)] bulletgraph(data_to_plot2, limits=[20, 60, 100, 160], labels=["Poor", "OK", "Good", "Excellent"], size=(8,5), Axis_label ="Performance Measure", label_color="black", bar_color="#252525", target_color='#f7f7f7', title=" Performance Measure")Copy the code

We can also make some optimizations to format the X-axis to display information more consistently

In the following example, we can measure the performance of a hypothetical company’s marketing budget

def money(x, pos): 'The two args are the value and tick position' return "${:,.0f}".format(x) money_fmt = FuncFormatter(money) data_to_plot3 = [("HR", 50000, 60000), ("Marketing", 75000, 65000), ("Sales", 125000, 80000), ("R&D", 195000, 115000)] palette = sns.light_palette("grey", 3, reverse=False) bulletgraph(data_to_plot3, limits=[50000, 125000, 200000], labels=["Below", "On Target", "Above"], size=(10,5), axis_label="Annual Budget", label_color="black", Bar_color ="#252525", target_color='# f7F7F7 ', palette=palette, title=" Marketing channel budget Performance ", formatter=money_fmt)Copy the code

It looks good, how about it? Let’s do it together!

If you like, click a “like” and follow me