web-dev-qa-db-ja.com

PythonでMatplotlibラインの外縁の輪郭をプロットする方法は?

networkxエッジにアウトライン(_linestyle=":"_)をプロットしようとしています。 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がデータ空間に直接変換されるとは思いません

または、これらのラインコレクションを四角形オブジェクトに変換できれば、それも可能です。

enter image description here

10
O.rka

特定の幅の線を別の線で囲む問題は、線がデータ座標で定義されているのに対し、線幅は物理的な単位、つまりポイント単位であるということです。データ幅やズームレベルなどに関係なく線幅を設定できるため、これは一般的に望ましいことです。また、軸のアスペクトに関係なく、線の端が常に線に垂直になるようにします。

したがって、線の輪郭は常に混合座標系にあり、最終的な外観は、レンダラーで実際の線を描く前に決定されません。したがって、(変更される可能性のある)座標を考慮に入れるソリューションの場合、図の現在の状態のアウトラインを決定する必要があります。

1つのオプションは、既存の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

もちろん、アイデアは、上記のlc1オブジェクトの代わりに、質問のlinecollectionオブジェクトを使用することです。これは、コードで置き換えるのに十分簡単なはずです。

LineCollection のオブジェクトには、明確なエッジカラーとフェースカラーがありません。線種を設定しようとすると、線分全体のスタイルに影響します。一連のパッチを使用すると、目的の効果を簡単に作成できることがわかりました。各パッチはグラフのエッジを表します。パッチのエッジカラー、ラインスタイル、ライン幅、フェースカラーは個別に操作できます。コツは、エッジを回転した長方形パッチに変換する関数を作成することです。

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位置を使用して回転角度を見つけることができることに気付きました。ここでも同じトリックを使用します。また、長方形の長さの大きさが負になることもあります。 Rectangle Patch は、xおよびy入力が長方形の左下隅を参照していることを前提としています。これが正しいことを確認するために簡単なチェックを実行します。 falseの場合、上部を最初に指定しています。その場合、長方形を同じ角度に沿って後方に描画します。

別の落とし穴:パッチを作成する前に、レイアウトアルゴリズムを実行することが重要です。 posを指定したら、エッジを使用して開始位置と停止位置を検索できます。

改善の機会:各パッチを生成するときにプロットする代わりに、 PatchCollection を使用してパッチを一括で操作できます。ドキュメントは、PatchCollectionの方が高速であると主張していますが、すべてのユースケースに適合するとは限りません。各パッチのプロパティを個別に設定したいという希望を表明したので、コレクションは必要な柔軟性を提供しない可能性があります。

1
SNygard