Каждый, кто хоть раз работал с Matplotlib, знает, насколько неэстетичными могут быть его графики по умолчанию. Давайте изучим несколько хитростей, которые помогут вашим визуализациям выделяться на фоне стандартных.
Мы начнем с простого линейного графика — одного из самых популярных типов визуализации. Основное внимание уделим добавлению градиентной заливки под графиком — задаче, реализация которой не так очевидна, как может показаться.
Для начала импортируем все необходимые библиотеки:
Для начала импортируем все необходимые библиотеки:
import pandas as pd
import numpy as np
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib import rcParams
from matplotlib.path import Path
from matplotlib.patches import PathPatch
np.random.seed(38) # Для воспроизводимости результатов
Теперь создадим данные для нашей визуализации: сгенерируем ряд, напоминающий то, как выглядит динамика цен на акции.
dates = pd.date_range(start='2024-02-01', periods=100, freq='D')
initial_rate = 75
drift = 0.003
volatility = 0.1
returns = np.random.normal(drift, volatility, len(dates))
rates = initial_rate * np.cumprod(1 + returns)
x, y = dates, rates
Посмотрим, как выглядит график с настройками Matplotlib по умолчанию:
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(dates, rates)
ax.xaxis.set_major_locator(mdates.DayLocator(interval=30))
plt.show()

Не особо впечатляет, правда? Но мы постепенно его улучшим.
Начнем с улучшения базовых элементов графика: добавим заголовок, настроим размер и шрифт, переместим метки оси Y вправо и изменим цвет, стиль и толщину основной линии:
Начнем с улучшения базовых элементов графика: добавим заголовок, настроим размер и шрифт, переместим метки оси Y вправо и изменим цвет, стиль и толщину основной линии:
fig, ax = plt.subplots(figsize=(10, 6)) # Устанавливаем размер графика
plt.title("Ежедневное количество посетителей", fontsize=18, color="black") # Добавляем заголовок
rcParams['font.family'] = 'DejaVu Sans' # Меняем шрифт
rcParams['font.size'] = 14 # Меняем размер шрифтв
ax.yaxis.tick_right() # Переносим метки оси Y на правую сторону
ax.yaxis.set_label_position("right") # Перемещаем подпись оси Y вправо
ax.plot(dates, rates, color='#268358', linewidth=2) # Изменяем цвет, стиль и толщину линии

Теперь график стал немного чище. Давайте ещё добавим минималистичную сетку на фон, уберем границы для более аккуратного вида и удалим деления с оси Y.
Вот как это можно сделать:
Вот как это можно сделать:
ax.grid(color="gray", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6) # Минималистичная сетка
ax.tick_params(axis="x", colors="black") # Установка цвета меток оси X
ax.tick_params(axis="y", left=False, labelleft=False) # Скрытие делений и меток слева
ax.spines["top"].set_visible(False) # Убираем верхнюю границу
ax.spines["right"].set_visible(False) # Убираем правую границу
ax.spines["bottom"].set_color("black") # Цвет нижней границы
ax.spines["left"].set_color("white") # Скрываем левую границу
ax.spines["left"].set_linewidth(1) # Толщина левой границы
ax.tick_params(axis="y", length=0) # Убираем деления с оси Y

Теперь добавим небольшую эстетическую деталь — год рядом с первой меткой на оси X. Также сделаем цвет меток делений более приглушенным:
def custom_date_formatter(t, pos, dates, x_interval):
"""
Функция для форматирования меток оси X.
Если это первая метка, добавляем год, иначе только дату.
"""
date = dates[pos * x_interval] # Получаем дату для текущей позиции
if pos == 0: # Для первой метки добавляем год
return date.strftime('%d %b \'%y') # Формат: "01 Feb '24"
else:
return date.strftime('%d %b') # Формат: "01 Feb"
# Применяем пользовательский форматтер к оси X
x_interval = 30 # Интервал между метками (в днях)
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, pos: custom_date_formatter(x, pos, dates=dates, x_interval=x_interval)
))
[t.set_color('#808079') for t in ax.yaxis.get_ticklabels()] # Цвет меток оси Y
[t.set_color('#808079') for t in ax.xaxis.get_ticklabels()] # Цвет меток оси X

Мы подошли к самому сложному и интересному моменту — созданию градиентной заливки под кривой.
В Matplotlib нет встроенной функции для создания градиентной заливки, но мы можем смоделировать её, создав градиентное изображение и обрезав его по форме графика. Это требует некоторой магии с использованием `Path`, `PathPatch` и `imshow`:
В Matplotlib нет встроенной функции для создания градиентной заливки, но мы можем смоделировать её, создав градиентное изображение и обрезав его по форме графика. Это требует некоторой магии с использованием `Path`, `PathPatch` и `imshow`:
# Градиентная заливка
numeric_x = np.array([i for i in range(len(x))])
numeric_x_patch = np.append(numeric_x, max(numeric_x))
numeric_x_patch = np.append(numeric_x_patch[0], numeric_x_patch)
y_patch = np.append(y, 0)
y_patch = np.append(0, y_patch)
# Создаем путь (path) для обрезки градиента
path = Path(np.array([numeric_x_patch, y_patch]).transpose())
patch = PathPatch(path, facecolor='none')
plt.gca().add_patch(patch)
# Добавляем градиентное изображение
ax.imshow(
numeric_x.reshape(len(numeric_x), 1),
interpolation="bicubic",
cmap=plt.cm.Greens,
origin='lower',
alpha=0.3,
extent=[min(numeric_x), max(numeric_x), min(y_patch), max(y_patch) * 1.2],
aspect="auto",
clip_path=patch,
clip_on=True
)

Теперь график выглядит чистым и стильным. Осталось добавить несколько финальных штрихов с помощью любого редактора (например, Google Slides): заголовок, скругленные углы границ и несколько числовых индикаторов.

Ниже полный код, который объединяет все шаги, описанные ранее 👇🏻
import pandas as pd
import numpy as np
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib import rcParams
from matplotlib.path import Path
from matplotlib.patches import PathPatch
np.random.seed(38)
# Генерация данных
dates = pd.date_range(start='2024-02-01', periods=100, freq='D')
initial_rate = 75
drift = 0.003
volatility = 0.1
returns = np.random.normal(drift, volatility, len(dates))
rates = initial_rate * np.cumprod(1 + returns)
x, y = dates, rates
# Общие параметры графика
fig, ax = plt.subplots(figsize=(10, 6))
plt.title("Daily visitors", fontsize=18, color="black")
rcParams['font.family'] = 'DejaVu Sans'
rcParams['font.size'] = 14
# Перемещение оси Y вправо
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
# Настройка осей
x_interval = 21
ax.xaxis.set_major_formatter(mdates.DateFormatter("%d %b"))
ax.xaxis.set_major_locator(mdates.DayLocator(interval=x_interval))
ax.yaxis.set_major_locator(ticker.MultipleLocator(50))
# Добавление сетки
ax.grid(color="gray", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6)
ax.tick_params(axis="x", colors="black")
ax.tick_params(axis="y", left=False, labelleft=False)
# Удаление границ
ax.spines["top"].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines["bottom"].set_color("black")
ax.spines['left'].set_color('white')
ax.spines['left'].set_linewidth(1)
# Удаление делений с оси Y
ax.tick_params(axis='y', length=0)
# Форматирование меток оси X
def custom_date_formatter(t, pos, dates, x_interval):
date = dates[pos * x_interval]
if pos == 0:
return date.strftime('%d %b \'%y')
else:
return date.strftime('%d %b')
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, pos: custom_date_formatter(x, pos, dates=dates, x_interval=x_interval)
))
# Цвет меток делений
[t.set_color('#808079') for t in ax.yaxis.get_ticklabels()]
[t.set_color('#808079') for t in ax.xaxis.get_ticklabels()]
# Градиентная заливка
numeric_x = np.array([i for i in range(len(x))])
numeric_x_patch = np.append(numeric_x, max(numeric_x))
numeric_x_patch = np.append(numeric_x_patch[0], numeric_x_patch)
y_patch = np.append(y, 0)
y_patch = np.append(0, y_patch)
path = Path(np.array([numeric_x_patch, y_patch]).transpose())
patch = PathPatch(path, facecolor='none')
plt.gca().add_patch(patch)
ax.imshow(
numeric_x.reshape(len(numeric_x), 1),
interpolation="bicubic",
cmap=plt.cm.Greens,
origin='lower',
alpha=0.3,
extent=[min(numeric_x), max(numeric_x), min(y_patch), max(y_patch) * 1.2],
aspect="auto",
clip_path=patch,
clip_on=True
)
# Построение основной линии
y_chart = y_patch
y_chart[0] = y_chart[1]
y_chart[-1] = y_chart[-2]
ax.plot(numeric_x_patch, y_chart, color='#268358', linewidth=2)
# Исправление серой линии от imshow
ax.plot([max(numeric_x_patch), max(numeric_x_patch)], [0, max(y)], color='white', linewidth=2)
# Сохранение графика
plt.savefig('high_quality_plot.png', dpi=300, bbox_inches='tight')
plt.show()
Источник: Medium