Как построить контур внешних краев на линии Matplotlib в Python?

Я пытаюсь нарисовать контур (linestyle = ":") на краях networkx. Я не могу понять, как это сделать с matplotlibpatch объектами? Кто-нибудь теперь знает, как манипулировать этими patch объектами, чтобы рисовать контуры этих «краев»? Если это невозможно, кто-нибудь знает, как заставить данные строки использовать ax.plot(x,y,linestyle = ":") отдельно, чтобы сделать это?

import networkx as nx
import numpy as np
from collections import *

# Graph data
G = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.8688325076457851)])), (1, OrderedDict([('weight', 0.13116749235421485)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.29660515972204304)])), ('y4', OrderedDict([('weight', 0.703394840277957)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.2858185316736193)])), ('y5', OrderedDict([('weight', 0.7141814683263807)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 1.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.27847763084646443)])), (5, OrderedDict([('weight', 0.7215223691535356)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.5733512797415756)])), (2, OrderedDict([('weight', 0.4266487202584244)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G = nx.from_dict_of_dicts(G)
G_scaffold = {'input': OrderedDict([('y1', OrderedDict())]), 'y1': OrderedDict([('y2', OrderedDict()), (1, OrderedDict())]), 'y2': OrderedDict([('y3', OrderedDict()), ('y4', OrderedDict())]), 1: OrderedDict(), 'y3': OrderedDict([(4, OrderedDict()), ('y5', OrderedDict())]), 'y4': OrderedDict([(3, OrderedDict()), (5, OrderedDict())]), 4: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict()), (2, OrderedDict())]), 3: OrderedDict(), 5: OrderedDict(), 6: OrderedDict(), 2: OrderedDict()}
G_scaffold = nx.from_dict_of_dicts(G_scaffold)
G_sem = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.046032370518141796)])), (1, OrderedDict([('weight', 0.046032370518141796)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.08764771571290508)])), ('y4', OrderedDict([('weight', 0.08764771571290508)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.06045928834718992)])), ('y5', OrderedDict([('weight', 0.06045928834718992)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 0.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.12254141747735424)])), (5, OrderedDict([('weight', 0.12254141747735425)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.11700701511079069)])), (2, OrderedDict([('weight', 0.11700701511079069)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G_sem = nx.from_dict_of_dicts(G_sem)

# Edge info
edge_input = ('input', 'y1')
weights_sem = np.array([G_sem[u][v]['weight']for u,v in G_sem.edges()]) * 256

# Layout
pos = nx.nx_agraph.graphviz_layout(G_scaffold, prog = "dot", root = "input")

# Plotting graph
pad = 10
with plt.style.context("ggplot"):
    fig, ax = plt.subplots(figsize=(8,8))
    linecollection = nx.draw_networkx_edges(G_sem, pos, alpha=0.5, edges=G_sem.edges(), arrowstyle = "-", edge_color = "#000000", width=weights_sem)
    x = np.stack(pos.values())[:,0]
    y =  np.stack(pos.values())[:,1]
    ax.set(xlim=(x.min()-pad,x.max()+pad), ylim=(y.min()-pad, y.max()+pad))

    for path, lw in zip(linecollection.get_paths(), linecollection.get_linewidths()):
        x = path.vertices[:,0]
        y = path.vertices[:,1]
        w = lw/4
        theta = np.arctan2(y[-1] - y[0], x[-1] - x[0])
    #     ax.plot(x, y, color = "blue", linestyle = ":")
        ax.plot((x-np.sin(theta)*w), y+np.cos(theta)*w, color = "blue", linestyle = ":")
        ax.plot((x+np.sin(theta)*w), y-np.cos(theta)*w, color = "blue", linestyle = ":")

После пары мысленных экспериментов я понял, что мне нужно рассчитать угол, а затем соответствующим образом отрегулировать колодки:

Например, если бы линия была полностью вертикальной (на 90 или -90), то координаты y не были бы сдвинуты вообще, а координаты x были бы сдвинуты. Противоположное произойдет для линии с углом 0 или 180.

Тем не менее, это все еще немного не так.

Подозреваю, что это актуально: matplotlib - Развернуть строку с указанной шириной в блоке данных?

Я не думаю, что linewidth напрямую переводится в пространство данных.

В качестве альтернативы, если бы эти наборы линий могли быть преобразованы в прямоугольные объекты, это также было бы возможно.

Как построить контур внешних краев на линии Matplotlib в Python?

Ширина линии остается постоянной по отношению к размерам в пикселях на вашем дисплее. Как только вы измените размер окна, ваше решение, которое изначально было «немного неправильным», станет очень «неправильным». Лично я бы не стал исправлять networkx, а просто рисовал бы объекты с координатами в пространстве данных.

Paul Brodersen 02.05.2019 13:29
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
11
1
2 399
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Объекты в LineCollection не имеют четкого цвета края и цвета лица. Пытаясь установить стиль линии, вы влияете на стиль всего сегмента линии. Мне было проще создать желаемый эффект, используя серию патчей. Каждый патч представляет ребро графа. Цветом края, стилем линий, шириной линии и цветом лицевой стороны фрагментов можно управлять по отдельности. Хитрость заключается в создании функции для преобразования края в повернутый патч Rectangle.

import matplotlib.path as mpath
import matplotlib.patches as mpatches
import numpy as np
from matplotlib import pyplot as plt
import networkx as nx

G = nx.Graph()
for i in range(10):
    G.add_node(i)
for i in range(9):
    G.add_edge(9, i)

# make a square figure so the rectangles look nice
plt.figure(figsize=(10,10))
plt.xlim(-1.1, 1.1)
plt.ylim(-1.1, 1.1)

def create_patch(startx, starty, stopx, stopy, width, w=.1):
    # Check if lower right corner is specified.
    direction = 1
    if startx > stopx:
        direction = -1

    length = np.sqrt((stopy-starty)**2 + (stopx-startx)**2)
    theta = np.arctan((stopy-starty)/(stopx-startx))
    complement = np.pi/2 - theta

    patch = mpatches.Rectangle(
        (startx+np.cos(complement)*width/2, starty-np.sin(complement)*width/2), 
        direction * length,
        width,
        angle=180/np.pi*theta, 
        facecolor='#000000', 
        linestyle=':', 
        linewidth=width*10,
        edgecolor='k',
        alpha=.3
    )
    return patch

# Create layout before building edge patches
pos = nx.circular_layout(G)

for i, edge in enumerate(G.edges()):
    startx, starty = pos[edge[0]]
    stopx, stopy = pos[edge[1]]
    plt.gca().add_patch(create_patch(startx, starty, stopx, stopy, (i+1)/10))

plt.show()

Image of width and linestyle changes.

В вашем примере вы заметили, что мы можем использовать положения ребер по осям X и Y, чтобы найти угол поворота. Здесь мы используем тот же трюк. Заметьте также, что иногда величина длины прямоугольника бывает отрицательной. Прямоугольный патч предполагает, что входные параметры x и y относятся к левому нижнему углу прямоугольника. Мы запускаем быструю проверку, чтобы убедиться, что это правда. Если false, мы сначала указали вершину. В этом случае мы рисуем прямоугольник назад под тем же углом.

Еще одна загвоздка: важно запустить алгоритм компоновки перед созданием патчей. Как только pos указан, мы можем использовать ребра для поиска начального и конечного местоположения.

Возможность улучшения: Вместо построения каждого патча по мере его создания вы можете использовать ПатчКоллекция и манипулировать патчами в пакетном режиме. В документах утверждается, что PatchCollection работает быстрее, но может не подходить для всех вариантов использования. Поскольку вы выразили желание устанавливать свойства для каждого патча независимо, коллекция может не обеспечивать необходимой вам гибкости.

Основное предположение здесь состоит в том, что график показан в масштабе с одинаковым аспектом (т. Е. Одна единица в направлении y имеет ту же длину, что и одна единица в направлении x). Согласно изображению в вопросе, это не обязательно так.

ImportanceOfBeingErnest 02.05.2019 22:07

Неплохо подмечено. Мне придется подумать, как это сделать с другими соотношениями сторон (в идеале нам не нужно заранее знать соотношение сторон...)

SNygard 03.05.2019 18:46

Когда я использую в своем примере: ZeroDivisionError: theta = np.arctan((stopy-starty)/(stopx-startx)) ZeroDivisionError: деление с плавающей запятой на ноль

O.rka 07.05.2019 21:11
Ответ принят как подходящий

Проблема окружения линии определенной ширины другой линией заключается в том, что линия определяется в координатах данных, а ширина линии выражается в физических единицах, а именно в точках. В целом это желательно, поскольку позволяет сделать ширину линии независимой от диапазона данных, уровня масштабирования и т. д. Это также гарантирует, что конец линии всегда будет перпендикулярен линии, независимо от положения осей.

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

Один из вариантов — использовать нового исполнителя, который принимает существующий LineCollection в качестве входных данных и создает новые преобразования в зависимости от текущего положения линий в пространстве пикселей.

Далее я выбрал PatchCollection. Начав с прямоугольника, мы можем масштабировать и поворачивать его, а затем переводить в положение исходной линии.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection, PatchCollection
import matplotlib.transforms as mtrans


class OutlineCollection(PatchCollection):
    def __init__(self, linecollection, ax=None, **kwargs):
        self.ax = ax or plt.gca()
        self.lc = linecollection
        assert np.all(np.array(self.lc.get_segments()).shape[1:] == np.array((2,2)))
        rect = plt.Rectangle((-.5, -.5), width=1, height=1)
        super().__init__((rect,), **kwargs)
        self.set_transform(mtrans.IdentityTransform())
        self.set_offsets(np.zeros((len(self.lc.get_segments()),2)))
        self.ax.add_collection(self)

    def draw(self, renderer):
        segs = self.lc.get_segments()
        n = len(segs)
        factor = 72/self.ax.figure.dpi
        lws = self.lc.get_linewidth()
        if len(lws) <= 1:
            lws = lws*np.ones(n)
        transforms = []
        for i, (lw, seg) in enumerate(zip(lws, segs)):
            X = self.lc.get_transform().transform(seg)
            mean = X.mean(axis=0)
            angle = np.arctan2(*np.squeeze(np.diff(X, axis=0))[::-1])
            length = np.sqrt(np.sum(np.diff(X, axis=0)**2))
            trans = mtrans.Affine2D().scale(length,lw/factor).rotate(angle).translate(*mean)
            transforms.append(trans.get_matrix())
        self._transforms = transforms
        super().draw(renderer)

Обратите внимание, что фактические преобразования рассчитываются только в draw время. Это гарантирует, что они принимают во внимание фактическое положение в пространстве пикселей.

Использование может выглядеть как

verts = np.array([[[5,10],[5,5]], [[5,5],[8,2]], [[5,5],[1,4]], [[1,4],[2,0]]])

plt.rcParams["axes.xmargin"] = 0.1
fig, (ax1, ax2) = plt.subplots(ncols=2, sharex=True, sharey=True)

lc1 = LineCollection(verts, color = "k", alpha=0.5, linewidth=20)
ax1.add_collection(lc1)

olc1 = OutlineCollection(lc1, ax=ax1, linewidth=2, 
                         linestyle = ":", edgecolor = "black", facecolor = "none")


lc2 = LineCollection(verts, color = "k", alpha=0.3, linewidth=(10,20,40,15))
ax2.add_collection(lc2)

olc2 = OutlineCollection(lc2, ax=ax2, linewidth=3, 
                         linestyle = "--", edgecolors=["r", "b", "gold", "indigo"], 
                        facecolor = "none")

for ax in (ax1,ax2):
    ax.autoscale()
plt.show()

Теперь, конечно, идея состоит в том, чтобы использовать объект linecollection из вопроса вместо объекта lc1 из приведенного выше. Это должно быть достаточно легко заменить в коде.

Наконец-то я понял, как адаптировать свой код для этого! Спасибо. Это был огромный ответ. Я знаю, что это должно было занять какое-то время. Я понятия не имел, что это будет так сложно, но то, что вы говорите, имеет большой смысл.

O.rka 09.05.2019 04:18

Есть ли у вас какие-либо предложения о том, как адаптировать это к nx.DiGraph объектам, которые вместо этого выводят FancyArrowPatch объекты? Я пытался использовать PatchCollection([FancyArrowPatches from nx.draw_networkx_edges]), но у меня получилось AttributeError: 'PatchCollection' object has no attribute 'get_segments'

O.rka 09.05.2019 21:15

Ты хочешь сказать, что у тебя вообще нет LineCollection? Или это новая проблема? FancyArrowPatches могут быть дугами или изогнуты иным образом, поэтому необходимо использовать совершенно другую стратегию.

ImportanceOfBeingErnest 09.05.2019 22:19

Когда я воспроизвел свои реальные данные в упрощенной форме для этого поста, я использовал nx.Graph вместо исходного nx.OrderedDiGraph. Я заставил ваш метод отлично работать для nx.Graph (еще раз спасибо), но не понял, что nx.OrderedDiGraph возвращает список FancyArrowPatches. Я почти адаптировал сценарий, чтобы включить его pastebin.com/raw/jzL92vdW, но я получаю странное смещение i.imgur.com/sQys7Jz.png в финальном графике.

O.rka 09.05.2019 23:13

На данный момент у меня нет работающего networkx, и, вероятно, я не смогу более подробно изучить его в течение следующей недели или около того.

ImportanceOfBeingErnest 09.05.2019 23:46

В итоге я использовал вариант arrows=False на nx.draw_networkx_edges, так что теперь он работает! Это не с FancyArrowPatch, но да ладно... это не обязательно для того, что я пытался сделать. Ваше здоровье

O.rka 10.05.2019 02:43

Другие вопросы по теме