python networkx + pyqtgraph实现交互式网络图

实现网络图中的节点可以随意拖动。

解决方案

  1. networkx_viewer:暂停开发比较久
    1. networkx
    2. tk
  2. 基于pyqtgraph
    1. 示例
# -*- coding: utf-8 -*-

import sys
import pyqtgraph as pg
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import numpy as np
import networkx as nx
import random
# Enable antialiasing for prettier plots
#pg.setConfigOptions(antialias=True)

class DrawGraph(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setWindowTitle('NetViz')
        self.mw = pg.PlotWidget()#background=QColor(255,255,255))
        self.mw.setAspectLocked(lock=True, ratio=1)
        self.setCentralWidget(self.mw)
        self.showMaximized()
        self.mw.getPlotItem().showAxis('bottom', show=False)
        self.mw.getPlotItem().showAxis('left', show=False)
        self.g = Graph()
        self.G=None
        self.mw.addItem(self.g)


        self.create_menu()


    def create_action(  self, text, slot=None, shortcut=None,
                        icon=None, tip=None, checkable=False,
                        signal="triggered()"):
        action = QAction(text, self)
        if icon is not None:
            action.setIcon(QIcon(":/%s.png" % icon))
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            action.triggered.connect(slot)
            #self.connect(action, SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action


    def create_menu(self):
        self.file_menu = self.menuBar().addMenu("&File")
        load_file_action = self.create_action("&Open Network File",
            shortcut="Ctrl+O", slot=self.open_network,tip="Open Network File")
        load_model_action = self.create_action("&Karate_club",
            slot=self.zk_network,tip="Using Karate club graph")
        quit_action = self.create_action("&Quit", slot=self.close,
            shortcut="Ctrl+Q", tip="Close the application")
        self.add_actions(self.file_menu, (load_file_action, None,load_model_action, quit_action))


    def add_actions(self, target, actions):
        for action in actions:
            if action is None:
                target.addSeparator()
            else:
                target.addAction(action)

    def open_network(self):
        file_choices = "Network File (*.gml)"
        path = QFileDialog.getOpenFileName(self, 'Open file', '', file_choices)
        if path:
            G=nx.read_gml(filename[0])
            self.on_draw(G)

    def zk_network(self):
        G=nx.karate_club_graph()
        self.on_draw(G)

########################################################
    def on_draw(self,G):
        self.G = G
        self.G.pos=nx.layout.spring_layout(self.G)
        poslist=[]
        texts=[]
        sizes=[]
        for node in sorted(self.G.nodes()):
            poslist.append(self.G.pos[node]) # node position
            texts.append(str(node)) # node label
            sizes.append(self.G.degree(node)*0.005) # node size
        pos = np.array(poslist, dtype=float)

        adjlist=[]
        linelist=[]
        for edge in self.G.edges():
            adjlist.append(edge)
            linelist.append((125,125,125,122,1))
        adj = np.array(adjlist)
        NodeColors=[QColor(random.randint(1,255), random.randint(1,255),random.randint(1,255)) for i in range(self.G.number_of_nodes())]
        lines = np.array(linelist, dtype=[('red',np.ubyte),('green',np.ubyte),('blue',np.ubyte),('alpha',np.ubyte),('width',float)])
        self.g.setData(pos=pos, adj=adj,pxMode=False,size=sizes,pen=lines,symbolPen=NodeColors,symbolBrush=NodeColors, text=texts)


class Graph(pg.GraphItem):
    def __init__(self):
        self.dragPoint = None
        self.dragOffset = None
        self.textItems = []
        pg.GraphItem.__init__(self)

    def setData(self, **kwds):
        self.text = kwds.pop('text', [])
        self.data = kwds
        if 'pos' in self.data:
            npts = self.data['pos'].shape[0]
            self.data['data'] = np.empty(npts, dtype=[('index', int)])
            self.data['data']['index'] = np.arange(npts)
        self.setTexts(self.text)
        self.updateGraph()

    def setTexts(self, text):
        for i in self.textItems:
            i.scene().removeItem(i)
        self.textItems = []
        for t in text:
            item = pg.TextItem(t)
            self.textItems.append(item)
            item.setParentItem(self)

    def updateGraph(self):
        pg.GraphItem.setData(self, **self.data)
        for i,item in enumerate(self.textItems):
            item.setPos(*self.data['pos'][i])


    def mouseDragEvent(self, ev):
        if ev.button() != Qt.LeftButton:
            ev.ignore()
            return

        if ev.isStart():
            pos = ev.buttonDownPos()
            pts = self.scatter.pointsAt(pos)
            if len(pts) == 0:
                ev.ignore()
                return
            self.dragPoint = pts[0]
            ind = pts[0].data()[0]
            self.dragOffset = self.data['pos'][ind] - pos
        elif ev.isFinish():
            self.dragPoint = None
            return
        else:
            if self.dragPoint is None:
                ev.ignore()
                return

        ind = self.dragPoint.data()[0]
        self.data['pos'][ind] = ev.pos() + self.dragOffset
        self.updateGraph()
        ev.accept()


def main():
    app = QApplication(sys.argv)
    form = DrawGraph()
    form.show()
    app.exec_()

if __name__ == "__main__":
    main()