Генерация цветовых градиентов для дашбордов 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()

Так будет выглядеть график для трех животных:

Box Plot для трех животных
Box Plot для трех животных

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

Box Plot для шестнадцати животных
Box Plot для шестнадцати животных

Чтобы уйти от буйства красок в ситуации, когда количество категорий заранее не известно, я создаю список цветов, который представляет собой «плавное перетекание» из первого цвета в последний в списке.

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 может выглядеть так:

Box Plot для шести видов животных
Box Plot для шести видов животных

Или так:

Box Plot для 17 видов животных
Box Plot для 17 видов животных

Такой генератор градиентов можно использовать и в утилитарных целях. Например, если есть еще одно измерение, по которому можно ранжировать наши сущности и тогда цвет позволит визуализировать еще один параметр.

Для начала нужно сгенерировать значения этого параметра. Наш список животных содержит 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()

В результате запуска кода должна получиться такая картина:

86acb1a9441dc22ae116a4dfb931ffb9.png9187b4a9e031e28ab9383d760ee4dbf5.png

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

На сегодня все)
Спасибо, что дочитали до конца.

© Habrahabr.ru