Skip to content

Commit 6410edc

Browse files
authored
Generalize and refactor colorblindfriendly.py (#155)
1 parent b05ee9f commit 6410edc

File tree

1 file changed

+201
-96
lines changed

1 file changed

+201
-96
lines changed

colorblindfriendly.py

Lines changed: 201 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
reference to consult when making all kinds of figures, not just those made
1717
using PyMOL.
1818
19-
The colors are:
19+
The "colorblind" color palette includes:
2020
2121
* cb_black
2222
* cb_orange
@@ -27,20 +27,38 @@
2727
* cb_vermillion (also: cb_red, cb_redorange, cb_red_orange)
2828
* cb_reddish_purple (also: cb_rose, cb_violet, cb_magenta)
2929
30+
Also added are two palettes from matplotlib, "viridis" and "magma", which
31+
are designed to be perceptually uniform in both color and black-and-white
32+
printouts. These are available as "viridis[1-11]", "magma[1-11]".
33+
3034
USAGE
3135
36+
With the PyMOL Script Repo installed and importable, import the module and
37+
set the colors:
38+
39+
```
3240
import colorblindfriendly as cbf
3341
3442
# Add the new colors
3543
cbf.set_colors()
3644
color myObject, cb_red
3745
38-
# Replace built-in colors with cbf ones
46+
# Replace built-in colors of same names with cbf ones
3947
cbf.set_colors(replace=True)
4048
color myOtherObject, yellow # actually cb_yellow
4149
4250
# Add a `cb_colors` menu item to the OpenGL GUI ([C] menu in the right panel)
4351
cbf.add_menu()
52+
```
53+
54+
Or, to use without installing, run the script directly from GitHub. This
55+
will add the colors and install GUI palette menus for all three default
56+
color palettes:
57+
58+
```
59+
run https://github.com/Pymol-Scripts/Pymol-script-repo/blob/master/colorblindfriendly.py
60+
color myObject, cb_red
61+
```
4462
4563
REQUIREMENTS
4664
@@ -53,7 +71,7 @@
5371
5472
LICENSE
5573
56-
Copyright (c) 2014-2017 Jared Sampson
74+
Copyright (c) 2014-2025 Jared Sampson
5775
5876
Permission is hereby granted, free of charge, to any person obtaining a copy
5977
of this software and associated documentation files (the "Software"), to deal
@@ -75,6 +93,13 @@
7593
7694
CHANGELOG
7795
96+
0.4.0 Add Palette and PaletteColor NamedTuples for cleaner declaration of
97+
color palettes. [2025-02-10]
98+
99+
0.3.0 Generalize the way colors and menus are efined and added, to
100+
enable the use of additional color palettes. Add Viridis and Magma
101+
palettes (contributed by Yehudi Bloch). [2021-10-27]
102+
78103
0.2.0 Complete overhaul for PyMOL 2.0 with conversion to module format.
79104
Now, setting the new `cb_*` color values requires a call to the
80105
`set_colors()` function after import. You can also now add a
@@ -83,98 +108,163 @@
83108
84109
'''
85110
from __future__ import print_function
111+
import math
112+
from typing import NamedTuple, Optional
86113

87114
__author__ = 'Jared Sampson'
88-
__version__ = '0.2.0'
115+
__version__ = '0.4.0'
89116

90117
import pymol
91118
from pymol import cmd
92119

93120

94-
# Color blind-friendly color list based on information found at:
95-
# http://jfly.iam.u-tokyo.ac.jp/html/color_blind/#pallet
96-
CB_COLORS = {
97-
'black': {
98-
'rgb': [0, 0, 0],
99-
'alt': None,
100-
},
101-
'orange': {
102-
'rgb': [230, 159, 0],
103-
'alt': None,
104-
},
105-
'sky_blue': {
106-
'rgb': [86, 180, 233],
107-
'alt': ['skyblue', 'light_blue', 'lightblue'],
108-
},
109-
'bluish_green': {
110-
'rgb': [0, 158, 115],
111-
'alt': ['bluishgreen', 'green'],
112-
},
113-
'yellow': {
114-
'rgb': [240, 228, 66],
115-
'alt': None,
116-
},
117-
'blue': {
118-
'rgb': [0, 114, 178],
119-
'alt': None,
120-
},
121-
'vermillion': {
122-
'rgb': [213, 94, 0],
123-
'alt': ['red', 'red_orange', 'redorange'],
124-
},
125-
'reddish_purple': {
126-
'rgb': [204, 121, 167],
127-
'alt': ['reddishpurple', 'rose', 'violet', 'magenta'],
128-
},
129-
}
121+
class PaletteColor(NamedTuple):
122+
'''Named tuple for storing color information.'''
123+
name: str
124+
rgb: tuple[int, int, int]
125+
alt_names: Optional[list[str]] = None
126+
# Allow code to be set explicitly in palette definition. This is helpful
127+
# for very dark colors, to allow contrast against the dark menu background.
128+
short_code: Optional[str] = None # for GUI menu
130129

130+
def all_names(self):
131+
'''Return a list of all names for this color.'''
132+
names = [self.name]
133+
if self.alt_names:
134+
names.extend(self.alt_names)
135+
return names
131136

132-
def set_colors(replace=False):
133-
'''Add the color blind-friendly colors to PyMOL.'''
134-
# Track the added colors
135-
added_colors = []
137+
def get_short_code(self):
138+
'''Return a 3-digit string approximating the RGB color.'''
139+
if self.short_code:
140+
return self.short_code
141+
return ''.join([str(math.floor(x / 256 * 10)) for x in self.rgb])
136142

137-
for color, properties in CB_COLORS.items():
138-
# RGB tuple shortcut
139-
rgb = properties['rgb']
140143

141-
# Get the primary and alternate color names into a single list
142-
names = [color]
143-
if properties['alt']:
144-
names.extend(properties['alt'])
144+
class Palette(NamedTuple):
145+
'''Named tuple for storing palette information.'''
146+
name: str
147+
colors: list[PaletteColor]
148+
prefix: str = ''
145149

146-
# Set the colors
147-
for name in names:
148-
# Set the cb_color
149-
cb_name = 'cb_{}'.format(name)
150-
cmd.set_color(cb_name, rgb)
150+
def install(self):
151+
'''Install the palette, adding colors and the GUI menu.'''
152+
PALETTES_MAP[self.name] = self
153+
add_menu(self.name)
151154

152-
# Optionally replace built-in colors
153-
if replace:
154-
cmd.set_color(name, rgb)
155-
spacer = (20 - len(name)) * ' '
156-
added_colors.append(' {}{}{}'.format(name, spacer, cb_name))
157-
else:
158-
added_colors.append(' {}'.format(cb_name))
159155

160-
# Notify user of newly available colors
161-
print('\nColor blind-friendly colors are now available:')
162-
print('\n'.join(added_colors))
163-
print('')
156+
# Color blind-friendly color list based on information found at:
157+
# http://jfly.iam.u-tokyo.ac.jp/html/color_blind/#pallet
158+
CB_COLORS = [
159+
PaletteColor('red', (213, 94, 0),
160+
['vermillion', 'red_orange', 'redorange']),
161+
PaletteColor('orange', (230, 159, 0)),
162+
PaletteColor('yellow', (240, 228, 66)),
163+
PaletteColor('green', (0, 158, 115),
164+
['bluish_green', 'bluishgreen']),
165+
PaletteColor('light_blue', (86, 180, 233),
166+
['lightblue', 'sky_blue', 'skyblue']),
167+
PaletteColor('blue', (0, 114, 178)),
168+
PaletteColor('violet', (204, 121, 167),
169+
['reddish_purple', 'reddishpurple', 'rose', 'magenta']),
170+
PaletteColor('black', (0, 0, 0), short_code='222'),
171+
]
172+
CB_PALETTE = Palette('colorblind', CB_COLORS, prefix='cb_')
173+
174+
# Viridis and Magma palettes contributed by Yehudi Bloch, originally
175+
# developed by Stéfan van der Walt and Nathaniel Smith for matplotlib.
176+
# https://matplotlib.org/stable/users/prev_whats_new/whats_new_1.5.html
177+
VIRIDIS_COLORS = [
178+
PaletteColor('viridis1', (253, 231, 36)),
179+
PaletteColor('viridis2', (186, 222, 39)),
180+
PaletteColor('viridis3', (121, 209, 81)),
181+
PaletteColor('viridis4', ( 66, 190, 113)),
182+
PaletteColor('viridis5', ( 34, 167, 132)),
183+
PaletteColor('viridis6', ( 32, 143, 140)),
184+
PaletteColor('viridis7', ( 41, 120, 142)),
185+
PaletteColor('viridis8', ( 52, 94, 141)),
186+
PaletteColor('viridis9', ( 64, 67, 135)),
187+
PaletteColor('viridis10', ( 72, 35, 116)),
188+
PaletteColor('viridis11', ( 68, 1, 84)),
189+
]
190+
VIRIDIS_PALETTE = Palette('viridis', VIRIDIS_COLORS)
191+
192+
MAGMA_COLORS = [
193+
PaletteColor('magma1', (251, 252, 191)),
194+
PaletteColor('magma2', (253, 205, 114)),
195+
PaletteColor('magma3', (253, 159, 108)),
196+
PaletteColor('magma4', (246, 110, 91)),
197+
PaletteColor('magma5', (221, 73, 104)),
198+
PaletteColor('magma6', (181, 54, 121)),
199+
PaletteColor('magma7', (140, 41, 128)),
200+
PaletteColor('magma8', ( 99, 25, 127)),
201+
PaletteColor('magma9', ( 59, 15, 111)),
202+
PaletteColor('magma10', ( 20, 13, 53)),
203+
PaletteColor('magma11', ( 0, 0, 3)),
204+
]
205+
MAGMA_PALETTE = Palette('magma', MAGMA_COLORS)
206+
207+
PALETTES_MAP = {
208+
CB_PALETTE.name: CB_PALETTE,
209+
VIRIDIS_PALETTE.name: VIRIDIS_PALETTE,
210+
MAGMA_PALETTE.name: MAGMA_PALETTE,
211+
}
212+
213+
214+
def _get_palettes(palette_name: Optional[str] = None):
215+
'''Return the desired Palette(s).'''
216+
if palette_name is None:
217+
return PALETTES_MAP.values()
218+
if palette_name not in PALETTES_MAP:
219+
raise ValueError(f'Palette "{palette_name}" not found.')
220+
else:
221+
return [PALETTES_MAP[palette_name]]
164222

165223

166-
def add_menu():
167-
'''Add a color blind-friendly list of colors to the PyMOL OpenGL menu.'''
224+
def set_colors(palette=None, replace=False):
225+
'''Add the color blind-friendly colors to PyMOL.'''
226+
palettes = _get_palettes(palette)
227+
for palette in palettes:
228+
added_colors = []
229+
for color in palette.colors:
230+
# RGB tuple shortcut
231+
rgb = color.rgb
232+
233+
# Set the colors
234+
for name in color.all_names():
235+
if palette.prefix:
236+
use_name = f'{palette.prefix}{name}'
237+
else:
238+
use_name = name
239+
cmd.set_color(use_name, rgb)
240+
241+
# Optionally replace built-in colors
242+
if replace:
243+
cmd.set_color(name, rgb)
244+
# FIXME hard-coded column width
245+
spacer = (20 - len(name)) * ' '
246+
added_colors.append(f' {name}{spacer}{use_name}')
247+
else:
248+
added_colors.append(' {}'.format(use_name))
249+
250+
# Notify user of newly available colors
251+
print(f'These {palette.name} colors are now available:')
252+
print('\n'.join(added_colors))
253+
254+
255+
def _add_palette_menu(palette: Palette):
256+
'''Add a color palette to the PyMOL OpenGL menu.'''
168257

169258
# Make sure cb_colors are installed.
170-
print('Checking for colorblindfriendly colors...')
259+
print(f'Checking for {palette.name} colors...')
171260
try:
172-
if cmd.get_color_index('cb_red') == -1:
173-
# mimic pre-1.7.4 behavior
174-
raise pymol.CmdException
261+
for color in palette.colors:
262+
if cmd.get_color_index(color.name) == -1:
263+
# mimic pre-1.7.4 behavior
264+
raise pymol.CmdException
175265
except pymol.CmdException:
176-
print('Adding colorblindfriendly colors...')
177-
set_colors()
266+
print(f'Adding {palette.name} palette colors...')
267+
set_colors(palette=palette.name)
178268

179269
# Abort if PyMOL is too old.
180270
try:
@@ -184,35 +274,50 @@ def add_menu():
184274
return
185275

186276
# Add the menu
187-
print('Adding cb_colors menu...')
277+
print(f'Adding {palette.name} menu...')
188278
# mimic pymol.menu.all_colors_list format
189279
# first color in list is used for menu item color
190-
cb_colors = ('cb_colors', [
191-
('830', 'cb_red'),
192-
('064', 'cb_green'),
193-
('046', 'cb_blue'),
194-
('882', 'cb_yellow'),
195-
('746', 'cb_magenta'),
196-
('368', 'cb_skyblue'),
197-
('860', 'cb_orange'),
198-
])
280+
281+
# Menu item for each color in the menu should be a tuple in the form
282+
# ('999', 'color_name')
283+
# where '999' is a string representing the 0-255 RGB color converted to
284+
# a 0-9 integer RGB format (i.e. 1000 colors).
285+
color_tuples = [
286+
(color.get_short_code(), palette.prefix + color.name)
287+
for color in palette.colors
288+
]
289+
menu_colors = (palette.name, color_tuples)
290+
199291
# First `pymol` is the program instance, second is the Python module
200292
all_colors_list = pymol.pymol.menu.all_colors_list
201-
if cb_colors in all_colors_list:
202-
print('Menu was already added!')
293+
if menu_colors in all_colors_list:
294+
print(f' - Menu for {palette.name} was already added!')
203295
else:
204-
all_colors_list.append(cb_colors)
205-
print(' done.')
296+
all_colors_list.append(menu_colors)
297+
print(' done.\n')
298+
206299

300+
def add_menu(palette_name=None):
301+
'''Add the specified color palettes to the PyMOL OpenGL menu.'''
302+
palettes = _get_palettes(palette_name)
303+
for palette in palettes:
304+
_add_palette_menu(palette)
207305

208-
def remove_menu():
209-
'''Remove the cb_colors menu.'''
306+
307+
def remove_menu(palette_name=None):
308+
'''Remove the color palette menu(s).'''
309+
palettes = _get_palettes(palette_name)
210310
all_colors_list = pymol.pymol.menu.all_colors_list
211-
if all_colors_list[-1][0] == 'cb_colors':
212-
all_colors_list.pop()
213-
print('The `cb_colors` menu has been removed.')
214-
else:
215-
print('The `cb_colors` menu was not found! Aborting.')
311+
for palette in palettes:
312+
initial_length = len(all_colors_list)
313+
all_colors_list[:] = [
314+
color_menu for color_menu in all_colors_list
315+
if color_menu[0] != palette.name
316+
]
317+
if len(all_colors_list) == initial_length:
318+
print(f'No menu for {palette.name} palette found. Nothing deleted.')
319+
else:
320+
print(f'Deleted menu for {palette.name} palette.')
216321

217322

218323
if __name__ == "pymol":

0 commit comments

Comments
 (0)