Understanding Color Vision Deficiency
Understanding Color Vision Deficiency
Color vision deficiency (CVD), commonly called color blindness, affects approximately 1 in 12 men (8%) and 1 in 200 women (0.5%). This means that in a typical business meeting, at least one person likely cannot distinguish certain colors in your charts. Designing for accessibility is not an edge case—it is designing for a significant portion of your audience.
The most common form of CVD is red-green color deficiency, which includes protanopia (reduced sensitivity to red light) and deuteranopia (reduced sensitivity to green light). For these viewers, red and green appear as similar shades of brown or yellow. The classic red-green palette used in traffic lights, financial charts, and status indicators is completely ineffective for them.
The second most common form is tritanopia (blue-yellow deficiency), which is much rarer but causes confusion between blue and green, and between yellow and violet. The rarest form is achromatopsia (total color blindness), where the viewer sees only shades of gray.
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
categories = ['A', 'B', 'C', 'D']
values = [35, 25, 20, 20]
colors_default = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3']
axes[0, 0].bar(categories, values, color=colors_default)
axes[0, 0].set_title('Default Palette (Problematic)')
colors_safe = ['#0072b2', '#e69f00', '#009e73', '#cc79a7']
axes[0, 1].bar(categories, values, color=colors_safe)
axes[0, 1].set_title('Colorblind-Safe Palette')
colors_gray = ['#000000', '#666666', '#999999', '#cccccc']
axes[1, 0].bar(categories, values, colors=colors_gray)
axes[1, 0].set_title('Grayscale (Universal)')
patterns = ['', '//', 'xx', '++']
for i, (cat, val, pat) in enumerate(zip(categories, values, patterns)):
axes[1, 1].bar(cat, val, color='white', edgecolor='black', hatch=pat)
axes[1, 1].set_title('Pattern-Based (Print-Safe)')
for ax in axes.flat:
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
The design principle that emerges from CVD is redundancy: never encode information through a single visual channel. If you use color to distinguish categories, also use position, shape, or direct labels. If you use color to show a trend, also use line style (solid vs. dashed) or direct annotation. Redundancy ensures that the chart remains readable even when one channel fails.
The tools for simulating CVD are widely available. The Color Oracle desktop application and the Coblis online simulator let you upload a chart and see how it appears to viewers with each type of CVD. Running your chart through a simulator before publishing takes seconds and catches problems that would otherwise exclude your audience.
Colorblind-Safe Palettes and Contrast Ratios
Colorblind-Safe Palettes and Contrast Ratios
The most widely recommended colorblind-safe categorical palette was developed by Bang Wong and published in Nature Methods. It uses eight colors that are distinguishable across all common forms of CVD: black, orange, sky blue, bluish green, yellow, blue, vermillion, and reddish purple. The hex values are optimized so that no two colors collapse into the same hue for any type of color vision deficiency.
import matplotlib.pyplot as plt
wong_palette = {
'black': '#000000',
'orange': '#E69F00',
'sky_blue': '#56B4E9',
'bluish_green': '#009E73',
'yellow': '#F0E442',
'blue': '#0072B2',
'vermillion': '#D55E00',
'reddish_purple': '#CC79A7'
}
fig, ax = plt.subplots(figsize=(10, 3))
for i, (name, hex_code) in enumerate(wong_palette.items()):
ax.barh(i, 1, color=hex_code, edgecolor='white')
ax.text(1.05, i, f'{name}: {hex_code}', va='center', fontsize=10)
ax.set_xlim(0, 2.5)
ax.set_ylim(-0.5, len(wong_palette) - 0.5)
ax.axis('off')
ax.set_title('Wong Colorblind-Safe Palette', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.show()
For sequential data (low to high), the viridis palette is the default choice. It was designed to be perceptually uniform (equal steps in data correspond to equal steps in perceived brightness) and colorblind-safe. The palette transitions from dark purple through blue, green, to yellow, maintaining distinguishable brightness levels across the entire range.
Contrast ratio is the quantitative measure of readability. The Web Content Accessibility Guidelines (WCAG) define contrast ratio as the ratio of luminance between two colors, ranging from 1:1 (no contrast) to 21:1 (black on white). WCAG Level AA requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18pt or 14pt bold). For chart elements, the same thresholds apply: axis labels, legends, and annotations must meet 4.5:1 against their background.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), color='#0072b2', linewidth=2.5, label='Series A')
ax.plot(x, np.cos(x), color='#E69F00', linewidth=2.5, linestyle='--', label='Series B')
ax.set_xlabel('Time (seconds)', fontsize=11)
ax.set_ylabel('Amplitude', fontsize=11)
ax.set_title('Accessible Line Chart with Colorblind-Safe Palette', fontsize=13, fontweight='bold')
ax.legend(frameon=False, fontsize=10)
ax.grid(axis='y', alpha=0.3, linestyle='--')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
The contrast ratio between the blue (#0072B2) and a white background is approximately 4.8:1, which passes WCAG AA. The orange (#E69F00) against white is approximately 2.9:1, which fails for text but passes for large graphical elements (the threshold is 3:1 for graphics). For text labels, use darker variants or black text on light backgrounds.
Designing for All Audiences: Beyond Color
Designing for All Audiences: Beyond Color
Accessibility in visualization extends far beyond color choice. It encompasses the entire viewing experience for people with different visual, cognitive, and physical abilities. Designing for the margins—the viewers with the least ability—improves the experience for everyone.
The first principle is never rely on color alone. This is the single most important rule in accessible visualization. If a line chart uses color to distinguish four series, also use different line styles (solid, dashed, dotted, dash-dot) and different marker shapes (circle, square, triangle, diamond). If a bar chart uses color to show performance vs. target, also use a reference line or pattern fill for the underperforming bars.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(10, 6))
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
series_a = [100, 120, 115, 140, 160, 155]
series_b = [80, 95, 88, 102, 110, 118]
series_c = [60, 72, 65, 78, 85, 80]
ax.plot(months, series_a, color='#0072B2', linestyle='-', marker='o',
linewidth=2, label='Product A')
ax.plot(months, series_b, color='#E69F00', linestyle='--', marker='s',
linewidth=2, label='Product B')
ax.plot(months, series_c, color='#009E73', linestyle='-.', marker='^',
linewidth=2, label='Product C')
ax.set_ylabel('Revenue (thousands USD)')
ax.set_title('Multi-Series Chart: Color + Style + Shape Redundancy')
ax.legend(frameon=False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
The second principle is sufficient font size. Axis labels should be at least 10pt, titles at least 14pt, and annotations at least 10pt. Smaller text is difficult to read for viewers with low vision and for anyone viewing the chart on a mobile device or projected screen. When in doubt, increase the font size—larger text rarely hurts readability.
The third principle is descriptive titles and alt text. A chart title should state the insight, not just the topic. 'Revenue Grew 12% After the March Campaign' is accessible; 'Monthly Revenue' is not. For screen reader users, the chart must have alt text that describes the key pattern: 'Line chart showing revenue increasing from $320K in January to $510K in June, with a notable acceleration in April following the marketing campaign launch.'
The fourth principle is avoid visual clutter. Gridlines, borders, and decorative elements that do not convey data create noise that makes the chart harder to parse for all viewers, especially those with cognitive disabilities. Remove every element that does not directly support the data message. The chart should contain only what is necessary to understand the insight.
Testing and Validating Accessibility
Testing and Validating Accessibility
Accessibility is not a design phase—it is a testing discipline. A chart that looks accessible to a designer with normal color vision may still fail for viewers with CVD, low vision, or cognitive disabilities. Systematic testing catches these failures before publication.
The first test is the color vision deficiency simulation. Upload your chart to a CVD simulator (Color Oracle, Coblis, or the built-in simulators in tools like Figma and Tableau) and check that all data series remain distinguishable. If two series collapse into the same color, replace one with a color from a different region of the palette or add a redundant encoding (line style, marker shape).
The second test is the contrast check. Use a contrast ratio calculator (WebAIM's contrast checker is the standard) to verify that all text elements meet WCAG AA (4.5:1 for text, 3:1 for graphical elements). Check text against its immediate background—axis labels against the plot area, legend text against the figure background, annotations against any fill colors they sit on.
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
x = np.arange(5)
values1 = [20, 35, 30, 45, 40]
values2 = [15, 25, 20, 35, 30]
width = 0.35
axes[0].bar(x - width/2, values1, width, label='Group A', color='#0072B2')
axes[0].bar(x + width/2, values2, width, label='Group B', color='#E69F00')
axes[0].set_xticks(x)
axes[0].set_xticklabels(['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])
axes[0].set_title('Color-Only Encoding (Risky)')
axes[0].legend(frameon=False)
axes[0].spines['top'].set_visible(False)
axes[0].spines['right'].set_visible(False)
axes[1].bar(x - width/2, values1, width, label='Group A', color='#0072B2')
axes[1].bar(x + width/2, values2, width, label='Group B', color='#E69F00',
hatch='//', edgecolor='white')
for i, (v1, v2) in enumerate(zip(values1, values2)):
axes[1].text(i - width/2, v1 + 1, str(v1), ha='center', fontsize=9)
axes[1].text(i + width/2, v2 + 1, str(v2), ha='center', fontsize=9)
axes[1].set_xticks(x)
axes[1].set_xticklabels(['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])
axes[1].set_title('Color + Pattern + Labels (Accessible)')
axes[1].legend(frameon=False)
axes[1].spines['top'].set_visible(False)
axes[1].spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
The third test is the grayscale print test. Print the chart in grayscale or convert it to grayscale digitally. If the chart is still readable—if all series, categories, and annotations remain distinguishable—then color is redundant, not essential. If the chart becomes unreadable in grayscale, the encoding relies too heavily on color and needs redundant channels.
The fourth test is the 5-second comprehension test with a colleague. Show the chart to someone for 5 seconds, then hide it and ask: What is the main message? Which data point is most important? If they cannot answer, the visual hierarchy is not working—regardless of how accessible the colors are. Accessibility includes cognitive accessibility: the chart must be easy to understand, not just easy to see.
The validation checklist before publishing any chart: (1) CVD simulation passed, (2) contrast ratios meet WCAG AA, (3) grayscale test passed, (4) font sizes are sufficient, (5) alt text is written, (6) the 5-second test passes. A chart that passes all six checks is accessible to the vast majority of viewers.
Practice Problems
You have a bar chart comparing 'On Track' (green) and 'At Risk' (red) project statuses. Convert it to a colorblind-safe palette while maintaining the semantic meaning.
Solution
import matplotlib.pyplot as plt
statuses = ['On Track', 'At Risk']
counts = [18, 7]
colors = ['#009E73', '#D55E00']
fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(statuses, counts, color=colors, edgecolor='white')
ax.set_title('Project Status (Colorblind-Safe)')
ax.set_ylabel('Number of Projects')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
for bar, count in zip(bars, counts):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
str(count), ha='center', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.show() You have a line chart with four product lines distinguished only by color. Add redundant encodings (line style and marker shape) so the chart works for colorblind viewers and grayscale printing.
Solution
import matplotlib.pyplot as plt
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
products = {
'Alpha': [100, 120, 115, 140, 160, 155],
'Beta': [80, 95, 88, 102, 110, 118],
'Gamma': [60, 72, 65, 78, 85, 80],
'Delta': [40, 48, 42, 55, 60, 58]
}
styles = ['-', '--', '-.', ':']
markers = ['o', 's', '^', 'D']
colors = ['#0072B2', '#E69F00', '#009E73', '#CC79A7']
fig, ax = plt.subplots(figsize=(10, 6))
for (name, values), style, marker, color in zip(products.items(), styles, markers, colors):
ax.plot(months, values, linestyle=style, marker=marker, color=color,
linewidth=2, markersize=6, label=name)
ax.set_ylabel('Revenue (thousands USD)')
ax.set_title('Product Revenue: Color + Style + Shape Redundancy')
ax.legend(frameon=False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show() Quiz
1. What percentage of men have some form of color vision deficiency?
2. What is the WCAG AA minimum contrast ratio for normal text?
3. What is the most important principle of accessible visualization?
Flashcards
Question
What is the Wong palette and why is it used?
Click to reveal answer
Answer
The Wong palette is an 8-color categorical palette (black, orange, sky blue, bluish green, yellow, blue, vermillion, reddish purple) designed to be distinguishable across all common forms of color vision deficiency. It is the standard for accessible scientific and business visualization.
Question
What is the grayscale print test?
Click to reveal answer
Answer
Convert the chart to grayscale (or print it without color). If all series, categories, and annotations remain distinguishable, color is redundant. If the chart becomes unreadable, the encoding relies too heavily on color and needs redundant channels (patterns, labels, line styles).
Question
What are the four accessibility principles beyond color choice?
Click to reveal answer
Answer
1) Never rely on color alone—use redundant encodings. 2) Sufficient font size (10pt minimum for labels, 14pt for titles). 3) Descriptive titles and alt text for screen readers. 4) Avoid visual clutter—remove elements that do not convey data.
Question
What is the viridis palette and when should you use it?
Click to reveal answer
Answer
Viridis is a perceptually uniform sequential palette that transitions from dark purple through blue, green, to yellow. It is colorblind-safe and maintains distinguishable brightness levels across the entire range. Use it for sequential data (low to high) such as heatmaps and continuous color scales.
Revision Notes
Key Takeaways
- 1. 8% of men have color vision deficiency—accessibility is not an edge case
- 2. The Wong palette provides 8 colors distinguishable across all common CVD types
- 3. Never rely on color alone; always add redundant encodings (style, shape, labels)
- 4. WCAG AA requires 4.5:1 contrast for text and 3:1 for graphical elements
- 5. The grayscale test is the quickest way to verify that color is redundant, not essential
Interview Tips
- • Mention specific palettes (Wong, viridis) rather than just saying 'use accessible colors'
- • Demonstrate the redundancy principle with concrete examples (line style + marker + color)
- • Reference WCAG guidelines by name and cite specific contrast ratios
- • Show that you test with CVD simulators and grayscale conversion before publishing
Cheat Sheet
Cheat Sheet: Accessible Visualization
Color Vision Deficiency
- 8% of men, 0.5% of women affected
- Most common: red-green (protanopia, deuteranopia)
- Design principle: redundancy—never use color alone
Colorblind-Safe Palettes
- Categorical: Wong palette (8 colors)
- Black #000000, Orange #E69F00, Sky Blue #56B4E9
- Bluish Green #009E73, Yellow #F0E442, Blue #0072B2
- Vermillion #D55E00, Reddish Purple #CC79A7
- Sequential: Viridis (perceptually uniform, CVD-safe)
WCAG Contrast Ratios
- Normal text: 4.5:1 minimum (AA)
- Large text (18pt+): 3:1 minimum (AA)
- Graphical elements: 3:1 minimum (AA)
Validation Checklist
- CVD simulation passed
- Contrast ratios meet WCAG AA
- Grayscale test passed
- Font sizes sufficient (10pt labels, 14pt titles)
- Alt text written
- 5-second comprehension test passed
Redundant Encodings
- Color + line style + marker shape
- Color + pattern fill + direct labels
- Position + size + annotation