Генерация цветовых градиентов для дашбордов Dash и отдельных графиков Plotly
Дисклеймер: эта статья написана исключительно в качестве демонстрации приемов и инструментов, которые применяет автор для построения визуализаций на python, и не содержит рекомендаций о правилах построения графиков.
С формальностями покончено, перейдем к сути. В моей работе мне необходимо поддерживать несколько интерактивных приложений-дашбордов, написанных с помощью библиотеки Dash. Они все разные, но у них есть нечто общее: в зависимости от выбранных параметров, дашборды визуализируют поведение разных сущностей, внутри которых разное количество категорий. Например, в разных регионах разное количество менеджеров, реализуется разное количество товарных групп и так далее. Кроме того, в любой момент это количество может увеличиться. Это создает сложности для применения выбранной цветовой схемы.
Задавать цвет для каждой категории вручную процесс зачастую трудоемкий и еще и неблагодарный: если завтра категорий станет на одну больше, нужно «руками» присвоить ей новый цвет. Если заранее создать список цветов, то он может однажды «закончиться», или наоборот будет использоваться пара-тройка цветов, которые ко всему прочему находятся необоснованно близко друг к другу.
Я возьму небольшую готовую цветовую схему. Далее станет понятно, зачем я инициализирую списки значений параметров rgb-кода.
light_green_rgb = [135, 182, 96]
light_green = '87b660'
light_blue_rgb = [64, 92, 162]
light_blue = '#405ca2'
dark_blue_rgb = [38, 47, 78]
dark_blue = '#262f4e'
neutral_white = '#f2f2f2'
Цвета выглядят так. Если интересно, как я делаю визуализацию цветовых схем, напишите, я напишу об этом.

Для описания моей мысли, я сначала создам данные. Они синтетические и касаются какого-то виртуального параметра, который теоретически можно измерить у животных.
import random
#Инициализирую список animals_list, содержащий рандомный список животных
animals_list = ['Лев', 'Дельфин', 'Павлин', 'Панда', 'Жираф', 'Коала', 'Фламинго', 'Кенгуру', 'Орангутан', 'Крокодил', 'Колибри', 'Белый медведь', 'Зебра', 'Бурый медведь', 'Тигр', 'Слон', 'Леопард', 'Белка', 'Черепаха', 'Попугай']
#Инициализирую списки случайных значений длиной 50
animal_count = len(animals_list)
lists = []
for _ in range(animal_count):
# Случайным образом задаю диапазон для генерации
min_range = random.randint(10, 30)
max_range = random.randint(50, 200)
# Генерируем список случайных чисел в выбранном диапазоне
lst = [random.randint(min_range, max_range) for _ in range(50)]
lists.append(lst)
Теперь создам Box Plot, содержащий рандомное количество видов животных.
fig = go.Figure()
count_animals = random.randint(1, 20)
for i in range(0, count_animals):
fig.add_trace(go.Box(y=lists[i], quartilemethod="linear", name=animals_list[i]))
fig.update_layout(height = 400, width = 800,
title_text=(f'Boxplot распределения значений вымышленного параметра для {count_animals} видов животных'),
title_font=dict(size=15), title_y=0.97,
paper_bgcolor=neutral_white, plot_bgcolor=neutral_white,
margin = dict(t=30, l=10, r=10, b=10),
xaxis_tickfont_size=10, yaxis_tickfont_size=10,
legend_font=dict(size=10))
fig.show()
Так будет выглядеть график для трех животных:

И вот так для 16:

Чтобы уйти от буйства красок в ситуации, когда количество категорий заранее не известно, я создаю список цветов, который представляет собой «плавное перетекание» из первого цвета в последний в списке.
import random
count_animals = random.randint(1, 20)
#создаю список цветов, длина которого равна рандомному значению count_animals
count = count_animals
animal_colors = []
color_step_1 = ((light_green_rgb[0]-light_blue_rgb[0]) / (count-1))
color_step_2 = ((light_green_rgb[1]-light_blue_rgb[1]) / (count-1))
color_step_3 = ((light_green_rgb[2]-light_blue_rgb[2]) / (count-1))
for i in range(count):
r = int(light_blue_rgb[0] + color_step_1 * i)
g = int(light_blue_rgb[1] + color_step_2 * i)
b = int(light_blue_rgb[2] + color_step_3 * i)
rgb = f'rgb({r}, {g}, {b})'
animal_colors.append(rgb)
fig = go.Figure()
for i in range(0, count_animals):
fig.add_trace(go.Box(y=lists[i], quartilemethod="linear", name=animals_list[i], marker_color = animal_colors[i]))
fig.update_layout(height = 400, width = 800,
title_text=(f'Boxplot распределения значений вымышленного параметра для {count_animals} видов животных'),
title_font=dict(size=15, color = dark_blue), title_y=0.97,
paper_bgcolor=neutral_white, plot_bgcolor=neutral_white,
margin = dict(t=30, l=10, r=10, b=10), xaxis_tickfont_size=10, yaxis_tickfont_size=10,
legend_font=dict(size=10))
fig.show()
Результат выполнения вышеописанного кода в зависимости от значения count_animals может выглядеть так:

Или так:

Такой генератор градиентов можно использовать и в утилитарных целях. Например, если есть еще одно измерение, по которому можно ранжировать наши сущности и тогда цвет позволит визуализировать еще один параметр.
Для начала нужно сгенерировать значения этого параметра. Наш список животных содержит 20 животных. Поэтому и значений этого параметра будет 20.
import random
#создаем список значений нового параметра
numbers = [random.randint(20, 100) for _ in range(20)]
#создаю DataFrame из всез имеющихся значений
df = pd.DataFrame(animals_list, columns = ['animal_name'])
#добавляю столбец со списком значений для Box Plot
df['values'] = lists
#добавляю параметр, по которому будем ранжировать DataFrame
df['ranking_parameter'] = numbers
#сразу ранжирую по столбцу 'ranking_parameter'
df = df.sort_values(by = 'ranking_parameter').reset_index()

Допустим, что в нашей вымышленной вселенной чем выше значение в столбце 'ranking_parameter', тем лучше, чем ниже, тем хуже и соответственно, это животное требует особого внимания.
Для такой индикации, нужно будет задать цвета для наилучшего и наихудшего значения. Традиционно, возьму оттенок зеленого для первого и оттенок красного для второго.
red = '#f24924'
red_rgb = [242, 73, 36]
green = '#32cd32'
green_rgb = [50, 205, 50]
Код в целом не очень отличается от предыдущего, разница только в том, что теперь я «тяну» данные из DataFrame, а не из списков. И, естественно, изменены имена переменных, которые хранят первый и последний цвет градиента.
count_animals = random.randint(1, 20)
count = count_animals
animal_colors = []
color_step_1 = ((green_rgb[0]-red_rgb[0]) / (count-1))
color_step_2 = ((green_rgb[1]-red_rgb[1]) / (count-1))
color_step_3 = ((green_rgb[2]-red_rgb[2]) / (count-1))
for i in range(count):
r = int(red_rgb[0] + color_step_1 * i)
g = int(red_rgb[1] + color_step_2 * i)
b = int(red_rgb[2] + color_step_3 * i)
rgb = f'rgb({r}, {g}, {b})'
animal_colors.append(rgb)
fig = go.Figure()
for i in range(0, count_animals):
fig.add_trace(go.Box(y=df.iloc[i]['values'], quartilemethod="linear", name=df.iloc[i]['animal_name'], marker_color = animal_colors[i]))
fig.update_layout(height = 400, width = 800,
title_text=(f'Boxplot распределения значений вымышленного параметра для {count_animals} видов животных'),
title_font=dict(size=15, color = dark_blue), title_y=0.97,
paper_bgcolor=neutral_white, plot_bgcolor=neutral_white,
margin = dict(t=30, l=10, r=10, b=10), xaxis_tickfont_size=10, yaxis_tickfont_size=10,
legend_font=dict(size=10))
fig.show()
В результате запуска кода должна получиться такая картина:


Такой подход дает возможность не только увидеть распределение данных по параметру_1, но и цветом визуально разделить наши сущности по параметру_2. При этом, цвет маркера первого и последнего животного всегда одинаковый, вне зависимости от количества животных, данные для которых визуализируются.
На сегодня все)
Спасибо, что дочитали до конца.