Approximately 4.5% of the world’s population is colorblind.
million people worldwide having just one type of visual impairment. The numbers get significantly higher if you were to take all conditions into account. Yet, it’s a rarely discussed topic.
As a data professional, you don’t want anyone misinterpreting your visuals. Sure, being extra clear is more work, but you’ll make a decent chunk of the population happier.
Today you’ll get 5 actionable tips for making your existing visualizations accessible.
Concrete Guidelines for Implementing Accessibility In Your Data Visualization
But first, let’s go over some general guidelines you should follow when Accessibility is a top priority.
Everything listed below is a curated and significantly shortened checklist of the A11Y project. If you’re wondering, “A11Y” is an abbreviation for “accessibility” (11 letters between “A” and “Y”).
Anyhow, here’s what you should pay attention to:
- Don’t rely on color to explain the data – A decent chunk of the population is color blind or suffers from some other visual impairment. Patterns are a way to go.
- If using color, go with darker, high-contrast tones – Light and low-contrast colors make it nearly impossible to distinguish between groups on a chart visually.
- Don’t hide important data behind interactions – Hover events are available only on the desktop. The majority of your users are on smartphones.
- Use labels and legends – Without them, the reader doesn’t know what the data represents.
- Translate data into clear insights – Simplify the data as much as possible, and then some. You don’t want anything to be open for interpretation.
- Provide context and explain the visualization – If feasible, annotate data points of interest, and add subtitle/caption.
- Have users with screen readers in mind – People with visual impairments use screen readers to navigate web pages. Use alt text to describe your embedded charts.
With these in mind, I came up with 5 actionable tweaks you can make to your visualizations right now.
Let’s dive into #1.
1. Use a High-Contrast or Colorblind-Friendly Color Palette
The easiest way to understand why color choice matters is by doing the wrong thing first.
Consider the following dataset:
x = np.array(["New York", "San Francisco", "Los Angeles", "Chicago", "Miami"])
y1 = np.array([50, 63, 40, 68, 35])
y2 = np.array([77, 85, 62, 89, 58])
y3 = np.array([50, 35, 79, 43, 67])
y4 = np.array([59, 62, 33, 77, 72])
It’s a perfect candidate for a stacked bar chart. In other words, to show office locations on the X-axis and stack employee counts on the Y-axis.
Now imagine you’re really into the color green.
You might want to color individual bar portions in different shades of green. It’s a horrible practice (except for some monochromatic color palettes), as you can see from the following:
plt.bar(x, y1, label="HR", color="#32a852")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#3ebd61")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#2bc254")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#44c767")
plt.title("[DON'T] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()
Many people wonder what their chart would look like if it was printed in a black-and-white book.
This one would look only marginally worse, but only because it looks horrendous from the get-go. Distinguishing between bar portions is challenging even for people without visual impairments.
You can use this website to check the contrast between two colors.
Let’s fix it by using a high-contrast color palette.
Custom High-Contrast Color Palette
I’ll continue with the assumption you like the color green.
Question: how can you create a high-contrast color palette from one color?
Answer: start with a dark shade and finish with a color similar enough to your primary color. In this case, yellow-gold is a perfect candidate.
You get the best of both worlds this way. You’re still using colors you like and the colors don’t have to get lighter (which would reduce the contrast) as you go through bar segments.
In practice, this boils down to playing around with the color
parameter for all segments:
plt.bar(x, y1, label="HR", color="#14342B")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#60935D")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#BAB700")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#F5E400")
plt.title("[DO] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

So much easier on the eyes.
Predefined Colorblind Color Palette
But consider the following scenarios:
- You don’t have the time to play around with different color combinations
- You do have the time, but there are about a dozen categories in your dataset (read: dozen colors to find)
There’s an easier solution to make your chart color scheme easier on the eyes while accounting for people with visual impairments.
One such solution is to use a colorblind-friendly color palette.
The first line of the snippet shows you how:
plt.style.use("tableau-colorblind10")
plt.bar(x, y1, label="HR")
plt.bar(x, y2, bottom=y1, label="Engineering")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales")
plt.title("[DO] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

This palette contains 10 colorblind-friendly colors, so it’s a good fit for charts with 10 groups or less.
If you need more, maybe you’ll be better off rethinking your visualization strategy.
2. Stop Using Colors – Use Patterns Instead
Another great way to remove any kind of misinterpretation from your charts is to use patterns instead of colors (or as an addition to colors).
Matplotlib has 10 hatch patterns you can choose from.
You can further customize the hatches by increasing their density or by combining multiple patterns. But that’s a topic for another time.
To implement patterns, add the hatch
parameter to plt.bar()
. The example below removes color altogether by setting fill=False
:
plt.bar(x, y1, label="HR", fill=False, hatch="*")
plt.bar(x, y2, bottom=y1, label="Engineering", fill=False, hatch="xx")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", fill=False, hatch="..")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", fill=False, hatch="//")
plt.title("[DO] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

Now there’s no way to misinterpret data on this chart.
Can You Mix Patterns with Color?
If you want the best of both worlds, color + pattern is where it’s at.
You’ll want to remove the fill=False parameter and change it with color
. Or, just copy the following code snippet:
plt.bar(x, y1, label="HR", color="#14342B", hatch="*")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#60935D", hatch="xx")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#BAB700", hatch="..")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#F5E400", hatch="//")
plt.title("[DO] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

Dark patterns are clearly visible on bar segments, but that might not always be the case.
The edgecolor
parameter controls the pattern color. Let’s see what happens after setting it to white:
plt.bar(x, y1, label="HR", color="#14342B", hatch="*", edgecolor="#FFFFFF")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#60935D", hatch="xx", edgecolor="#FFFFFF")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#BAB700", hatch="..", edgecolor="#FFFFFF")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#F5E400", hatch="///", edgecolor="#FFFFFF")
plt.title("[MAYBE] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

The pattern is visible for HR and Engineering departments, but the two on the top are a different story.
You might have no trouble seeing the lines on the topmost chart segment, but put yourself in the shoes of a person with visual impairments. They should always be your frame of reference.
Remember: Light-colored patterns work well on dark backgrounds. Dark-colored patterns work well on light backgrounds. Adjust accordingly.
3. Don’t Overwhelm User with the Information
This principle goes in two directions:
- Don’t put too much information on a single chart
- Don’t put too many charts next to each other, e.g., in your applications/dashboards
Doing both simultaneously is somewhat of an ultimate sin in data visualization.
Let’s start by adding a couple more departments into the mix.
The data is getting difficult to manage with Python lists, so I’ve opted for a Pandas DataFrame instead:
import pandas as pd
df = pd.DataFrame({
"HR": [50, 63, 40, 68, 35],
"Engineering": [77, 85, 62, 89, 58],
"Marketing": [50, 35, 79, 43, 67],
"Sales": [59, 62, 33, 77, 72],
"Customer Service": [31, 34, 61, 70, 39],
"Distribution": [35, 21, 66, 90, 31],
"Logistics": [50, 54, 13, 71, 32],
"Production": [22, 51, 54, 28, 40],
"Maintenance": [50, 32, 61, 69, 50],
"Quality Control": [20, 21, 88, 89, 39]
}, index=["New York", "San Francisco", "Los Angeles", "Chicago", "Miami"])
df

Now, using the colorblind-friendly palette, let’s plot the employee count per location and department as a stacked bar chart. To make things extra crammed, I’ve also thrown text counts into the mix:
plt.style.use("tableau-colorblind10")
ax = df.plot(kind="bar", stacked=True)
for container in ax.containers:
ax.bar_label(container, label_type="center", fontsize=10, color="#000000", fontweight="bold")
plt.title("[DON'T] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(title="Department", bbox_to_anchor=(1.05, 1), loc='upper left', ncol=1)
plt.show()

Now that’s just ugly.
Fix #1 – Present Less Information
One way to solve this unpresentable mess is by showing less information to the user.
For example, only show employee count in one city (across departments). You can then add a dropdown menu to the side of the chart so the user can control the office location.
The following snippet plots employees per department in Chicago as a horizontal bar chart:
chicago_data = df.loc["Chicago"].sort_values()
bars = plt.barh(chicago_data.index, chicago_data.values, color="#60935D", edgecolor="#000000")
for bar in bars:
plt.text(bar.get_width() + 2, bar.get_y() + bar.get_height() / 2, f"{int(bar.get_width())}", va="center", ha="left", fontsize=14, color="#000000")
plt.title("[DO] Employee Count by Department in Chicago", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Count")
plt.ylabel("Department")
plt.show()

Fix #2 – Reorganize the Data
If showing less information isn’t an option, maybe you can transpose your data.
For example, we’re dealing with 5 office locations and 10 departments. Showing 10 columns instead of 10 bar segments is easier on the eyes.
This way, you’ll end up showing office locations as bar segments instead of departments:
df_transposed = df.T
df_sorted = df_transposed.loc[df_transposed.sum(axis=1).sort_values().index]
ax = df_sorted.plot(kind="barh", width=0.8, edgecolor="#000000", stacked=True)
for container in ax.containers:
ax.bar_label(container, label_type="center", fontsize=10, color="#FFFFFF", fontweight="bold")
plt.title("[DO] Employee Count Per Location And Department", loc="left", fontdict={"weight": "bold"}, y=1.06)
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.show()

It’s just a matter of reframing the problem.
The chart on Image 10 is miles ahead of the chart on Image 8. It’s a fact. No one can argue with it.
4. Provide In-Depth Explanations of Data On Your Charts
You can leverage subtitle and/or caption sections of your chart to add extra information.
This comes in handy when you want to provide more context about the data, cite sources, or summarize the main point(s) of your visualization. The last one is most applicable for people with visual impairments.
The problem with matplotlib is that it doesn’t have a dedicated function for chart subtitles and captions. You can use suptitle(), but you’ll have to play around with x and y-axis coordinates.
Here’s an example:
plt.bar(x, y1, label="HR", color="#14342B", hatch="*")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#60935D", hatch="xx")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#BAB700", hatch="..")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#F5E400", hatch="//")
plt.suptitle("Chart shows how the employees are distributed per department and per office location.\nChicago office has the most employees.", x=0.125, y=0.98, ha="left", fontsize=14, fontstyle="italic")
plt.title("Employee Count Per Location And Department", fontsize=20, fontweight="bold", y=1.15, loc="left")
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

If you prefer a caption over a subtitle, you only have the change y-axis coordinate in plt.suptitle()
:
plt.bar(x, y1, label="HR", color="#14342B", hatch="*")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#60935D", hatch="xx")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#BAB700", hatch="..")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#F5E400", hatch="//")
plt.suptitle("Chart shows how the employees are distributed per department and per office location.\nChicago office has the most employees.", x=0.125, y=0, ha="left", fontsize=14, fontstyle="italic")
plt.title("Employee Count Per Location And Department", fontsize=20, fontweight="bold", y=1.06, loc="left")
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.show()

All in all, a subtitle or a caption may be the deciding factor in correctly getting your message to a person with visual impairments.
Just don’t make it 10 paragraphs long. Otherwise, it’s the 3rd point of this article all over again.
5. Add Alt Text When Embedding Plots
Many people with visual impairments use screen readers.
The problem with screen readers and charts is that they simply can’t coexist. They might be able to pick up textual elements from the graph, but they can’t interpret the visual content. So, whenever you’re sharing your visualizations (e.g., embedding them into a website), you must add alt text.
This is a paragraph the screen reader will read to your user.
To demonstrate, let’s use the plt.savefig()
function to save the chart as an image:
plt.bar(x, y1, label="HR", color="#14342B", hatch="*")
plt.bar(x, y2, bottom=y1, label="Engineering", color="#60935D", hatch="xx")
plt.bar(x, y3, bottom=y1 + y2, label="Marketing", color="#BAB700", hatch="..")
plt.bar(x, y4, bottom=y1 + y2 + y3, label="Sales", color="#F5E400", hatch="//")
plt.suptitle("Chart shows how the employees are distributed per department and per office location.\nChicago office has the most employees.", x=0.125, y=0, ha="left", fontsize=14, fontstyle="italic")
plt.title("Employee Count Per Location And Department", fontsize=20, fontweight='bold', y=1.06, loc="left")
plt.xlabel("Office Location")
plt.ylabel("Count")
plt.legend(loc="upper right", ncol=4)
plt.ylim(top=320)
plt.savefig("figure.jpg", dpi=300, bbox_inches="tight")
In a new HTML document, add an
tag that points to the image. This is where you should provide alt text:
Document

You can’t see alt text when you open the HTML file, but that’s because you’re not using a screen reader.
If the screen reader is detected, the alt text will be automatically read to the user.
The best you can do is use a screen reader plugin or point to the image that doesn’t exist in HTML:


Now the image can’t be found, so alt text is displayed instead.
Summing Up Data Visualization Accessibility
And there you have it — 5 things you should always keep in mind when designing data visualizations.
These tips are helpful in general but are of vital importance when accessibility is essential. And it always should be. It requires a tiny bit of extra work from your end, but makes your findings accessible to millions of additional people worldwide.