Как нарисовать контур внешних ребер на линии Matplotlib в Python? - PullRequest
10 голосов
/ 30 апреля 2019

Я пытаюсь нанести контур (linestyle=":") на края networkx. Кажется, я не могу понять, как это сделать с matplotlib patch объектами? Кто-нибудь теперь знает, как манипулировать этими 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 напрямую переводится в пространство данных

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

enter image description here

Ответы [ 2 ]

4 голосов
/ 08 мая 2019

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

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

Один из вариантов - использовать нового исполнителя, который принимает существующий 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()

enter image description here

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

1 голос
/ 02 мая 2019

Объекты в 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 и манипулировать патчами навалом. В документах утверждается, что PatchCollection работает быстрее, но может не подходить для всех случаев использования. Поскольку вы выразили желание устанавливать свойства для каждого исправления независимо, коллекция может не обеспечивать необходимую гибкость.

...