A Bull’s-eye diagram in general terms, is a conceptual model designed to bring focus to the most important items from a collection of items. The diagram helps us prioritize various tasks and therefore often used in strategic decision-making and brainstorming situations.

As on a dartboard, the diagram has an inner most smallest circle, and multiple outer rings surrounding it with increasing diameter. The innermost circle contains the highest-priority or most important items, the middle circle contains medium-priority items, and the largest circle contains the lowest-priority items. Because the inner-most circle, the bull’s-eye has the smallest area, it forces us to be very selective which items really should belong there. It’s simple to use on white board, on paper, and as I’ll show you here, even programmatically.
My implementation is in Python as Excel does not provide such a chart. Creating a bull’s-eye chart like this in Excel would be challenging due to a few limitations and reasons:
- Excel doesn’t provide native support for creating bull’s-eye charts, so we’d need to manipulate other chart types
(like radar or scatter plots) to approximate the look and feel.
This process can be cumbersome and doesn’t offer the same level of customization as Python. - Drawing concentric circles with gradient fills and ensuring they don’t overlap would be difficult to achieve in Excel.
Python, with its Matplotlib library, makes this task enjoyable and flexible. - Adjusting the colors based on the number of circles and achieving a smooth gradient would require complex Excel formulas and possibly VBA scripting.
In Python, we can accomplish this elegantly with a few lines of code.
Python Implementation & Examples:
The charts that are generated below at real-time are all data-driven, meaning each item on the charts has an associated value or weight. That determines the location of the item, specifically, in which ring do they belong. The label and values are structured in a Python dictionary object. Then final charting is done with matplotlib library. Along the way, there’s some geometrical calculations necessary for determining the datapoints locations on the plot, and because the smallest circle carries the highest weight, we also have do some calculations to ensure we plot from most important from center going outward as we plot the remaining data points with decreasing importance.
The diagram or chart can we helpful in visualizing data in many different contexts. For example:
- We can view a comparison of a few salespeople and their actual sales versus how close they are to the target. This will be demonstrated in Sales Target chart below.
- We can visualize a list of To-do items in terms of importance or desired order. This is demonstrated in To-do chart.
- Similarly, we can use it for bucket lists by importance or chronology; for showing proximity, daily routing, favorite things, and much more. Below are are some of those examples.
Sales Target: Click on the widget to generate the chart below.
To-do: Click on the widget to generate the chart below.
Protection Ring: Click on the widget to generate the chart below.
Bucket List: Click on the widget to generate the chart below.
Favorite Fast Food: Click on the widget to generate the chart below.
Proximity: Click on the widget to generate the chart below.
Routine: Click on the widget to generate the chart below.
Implementation Details:
The dictionary object for Sales Target above is defined as:data = {'category': ['Alex', 'Barb', 'Caleb', 'Dima', 'Eka'],
'value': [20, 25, 33, 80, 89], 'title': "Sales Target"}
It’s then converted into a pandas dataframe for ease of manipulation. df = pd.DataFrame(data)
df['inverse_value'] = 100 - df['value']
To determine the number of rings required for each diagram, and the radius, we need to get the maximum value with: max_value = df['inverse_value'].max() # df is the pandas dataframe
We’re grabbing the maximum value from the inverse_value column that we generated.
To get the number of rings in the diagram, we can use the number of elements in our category (as defined in the dictionary object): numrings = len(data['category']) + 1
And another instruction: radii = np.linspace(max_value, 0, numrings)
generates a list of values starting from max_value, going down to 0, with numrings total values. This is all a set up for a radial plot, where circles or distances in-between are spaced between max_value and 0.
To make it more interesting, I use a custom color map with gradience so each circle is filled with a some color. For example: colors = ['lightgreen', 'lightyellow', 'yellow', 'lightsalmon', 'lightcoral']
and then apply the colors with: cmap = LinearSegmentedColormap.from_list("custom_cmap", colors, N=numrings)
Then I draw the concentric circles with gradient color from outermost to the innermost rings with:for i, radius in enumerate(radii):
circle = plt.Circle((0, 0), radius, color=cmap(i / (numrings - 1)), fill=True)
ax.add_artist(circle)
The data points are plotted in a for loop with:for i, row in df.iterrows():
angle = i * (360 / len(df))
x = row['inverse_value'] * np.cos(np.deg2rad(angle))
y = row['inverse_value'] * np.sin(np.deg2rad(angle))
ax.plot(x, y, 'o', label=row['category'])
ax.text(x, y, row['category'], fontsize=12, ha='center', va='bottom')
Finally, the limits of the circle perimeters are set to the properly draw the rings with: ax.set_xlim(-max_value, max_value)
ax.set_ylim(-max_value, max_value)
You get the idea. It can be very flexible and powerful with some Python as its reinforcement.
I hope you found this post helpful and interesting. Explore this site for more tips and articles. Be sure to also check out my Patreon site where you can find free downloads and optional fee-based code and documentation. Thanks for visiting!