2025, Sep 22 22:01
Как сделать один разрыв линии в Matplotlib без штрихов с NaN
Надёжный способ сделать точный разрыв линии в Matplotlib в выбранной точке данных: вместо шаблонов штрихов — маскирование NaN и граничные вершины в данных.
Создать в линии Matplotlib один, точно расположенный разрыв, подстраивая шаблон штрихов, кажется простой задачей — пока не вступают в игру разные системы координат. Когда цель — получить аккуратный разрыв в выбранной точке данных, работа со штрихами в координатах отображения быстро становится ненадёжной. Есть более устойчивый способ добиться того же визуального результата без догадок о пиксельных расстояниях.
Постановка задачи
Нужно вычислить последовательность штрихов так, чтобы сплошная линия содержала один разрыв, центрированный в конкретной точке на этой линии. Базовый подход переводит координаты данных в координаты отображения и там вычисляет расстояния для построения последовательности штрихов.
import matplotlib.pyplot as mp
from matplotlib.lines import Line2D
import numpy as np
def compute_gap_dashes(ax_obj, seg, hit_pt):
    to_disp = lambda xy: ax_obj.transData.transform(xy)
    x_d, y_d = to_disp(np.array(seg.get_data()).T).T
    xh, yh = to_disp(hit_pt)
    dist_start = np.sqrt((x_d[0] - xh) ** 2 + (y_d[0] - yh) ** 2)
    full_len = np.sqrt((x_d[0] - x_d[1]) ** 2 + (y_d[0] - y_d[1]) ** 2)
    gap_size = full_len / 10
    pattern = (dist_start - (gap_size / 2), gap_size, full_len)
    return pattern
fig, axs = mp.subplots()
ln = Line2D((0, 1), (0, 2), zorder=0)
axs.add_line(ln)
target = (0.3, 0.6)
axs.scatter(*ln.get_data(), c='g', zorder=1)
axs.scatter(*target, c='r', zorder=1)
dash_pattern = compute_gap_dashes(axs, ln, target)
ln.set_dashes(dash_pattern)
Почему разрыв «уплывает»
Последовательности штрихов в Matplotlib измеряются в пунктах, а не в пикселях. Когда расстояния смешиваются между координатами данных и координатами отображения, получившийся шаблон часто не совпадает с задуманный позицией на данных. Даже при аккуратных преобразованиях попытка центрировать разрыв в заданной точке данных, подгоняя массив штрихов, остаётся хрупкой: небольшие изменения размера фигуры, DPI или трансформаций сдвигают фактический разрыв.
Более чистый подход: разрезать линию с помощью маскирования nan
Вместо подбора интервалов штрихов вставьте две граничные вершины вокруг целевой точки и используйте NaN, чтобы разорвать путь. В итоге получается одна нарисованная линия с реальным разрывом вокруг выбранной координаты данных. Ширина разрыва задаётся как доля расстояния по оси x между соседними вершинами вокруг цели, а собственно разрыв создаётся присваиванием целевой точке значения y равного NaN.
import matplotlib.pyplot as mp
import numpy as np
def carve_gap_limits(verts, idx_hit, hole_frac=0.1):
    """
    Создать две граничные точки вокруг verts[idx_hit] так,
    чтобы разрыв занимал долю hole_frac по оси x между
    предыдущей и следующей вершинами.
    """
    hit_xy = verts[idx_hit]
    prev_xy = verts[idx_hit - 1]
    next_xy = verts[idx_hit + 1]
    pos_rel = (hit_xy[0] - prev_xy[0]) / (next_xy[0] - prev_xy[0])
    left_rel = max(0, pos_rel - hole_frac / 2)
    right_rel = min(1, pos_rel + hole_frac / 2)
    left_xy = (1 - left_rel) * prev_xy + left_rel * next_xy
    right_xy = (1 - right_rel) * prev_xy + right_rel * next_xy
    return left_xy, right_xy
pts = np.array([[0, 0], [0.3, 0.6], [1, 2]])
idx = 1  # индекс целевой точки в pts
hole_frac = 0.1  # доля для ширины разрыва
is_cross = np.arange(len(pts)) == idx
left_xy, right_xy = carve_gap_limits(pts, idx, hole_frac=hole_frac)
aug = pts.copy()
aug[idx, 1] = np.nan  # убрать сегмент вокруг целевой точки
aug = np.insert(aug, [idx, idx + 1], [left_xy, right_xy], axis=0)
fig, ax = mp.subplots()
ax.plot(*aug.T)
ax.scatter(*pts[~is_cross, :].T, c="g")
ax.scatter(*pts[is_cross, :].T, c="r")
mp.show()
Так вы получаете разрыв именно там, где нужно, без подгонки массивов штрихов и без забот о расстояниях в пространстве отображения.
Зачем это знать
Опора на интервал штрихов смешивает системы координат и делает картинку хрупкой. Если выражать разрыв прямо в координатах данных, замысел остаётся привязанным к самим данным и не зависит от изменений фигуры или DPI. Есть и практический плюс: при маскировании NaN вся «прерванная» линия остаётся одним объектом Line2D, что упрощает логику построения. Если же вы решите рисовать сплошные участки по частям, можно строить несколько линий; при большом числе сегментов LineCollection будет эффективнее, чем набор множества объектов Line2D.
Вывод
Когда нужно намеренно сделать разрыв в линии в конкретной координате данных, не манипулируйте шаблонами штрихов. Вставьте граничные вершины вокруг цели и используйте маскирование NaN, чтобы «разрезать» путь. Помните: последовательности штрихов в Matplotlib задаются в пунктах, и именно смешение пространств делает подход со штрихами ненадёжным. Держите логику разрыва в пространстве данных, а к LineCollection обращайтесь, только если переходите к кусочным сегментам и вам нужна эффективность пакетной коллекции.
Статья основана на вопросе на StackOverflow от Macklin Entricken и ответе Ben Grossmann.