Browse Source

feat: export special graphml for yEd

master
Dnomd343 1 week ago
parent
commit
5fcc3a4fe1
  1. 2
      misc/all-graph/02-dump_igraph.py
  2. 230
      misc/all-graph/03-convert_graphml.py

2
misc/all-graph/02-dump_igraph.py

@ -82,7 +82,7 @@ def convert_all(json_dir: str, output_dir: str) -> None:
pool = multiprocessing.Pool() pool = multiprocessing.Pool()
for name in sorted(os.listdir(json_dir)): for name in sorted(os.listdir(json_dir)):
json_file = f'{json_dir}/{name}' json_file = f'{json_dir}/{name}'
output_prefix = f"{output_dir}/{name.removesuffix('.json')}" output_prefix = f'{output_dir}/{name.removesuffix('.json')}'
pool.apply_async(convert_and_dump, args=(json_file, output_prefix)) pool.apply_async(convert_and_dump, args=(json_file, output_prefix))
pool.close() pool.close()
pool.join() pool.join()

230
misc/all-graph/03-convert_graphml.py

@ -2,38 +2,159 @@
from __future__ import annotations from __future__ import annotations
import io
import os import os
import re from typing import override
from abc import ABC, abstractmethod
import numpy as np
import igraph as ig import igraph as ig
from lxml import etree from lxml import etree
import matplotlib.pyplot as plt
from dataclasses import dataclass from dataclasses import dataclass
import matplotlib.colors as mcolors
class GraphML: @dataclass(frozen=True)
@dataclass(frozen=True) class Node:
class Node: id: str
id: str code: str
code: str step: int
step: int
@dataclass(frozen=True)
class Edge:
src: str # node id
dst: str # node id
@dataclass(frozen=True)
class Edge:
src: str # node id
dst: str # node id
class Builder(ABC):
def __init__(self, _: list[Node]):
pass
@abstractmethod
def nsmap(self) -> dict[str | None, str]:
pass
@abstractmethod
def build_keys(self) -> list[etree.Element]:
pass
@abstractmethod
def build_node(self, node: Node) -> etree.Element:
pass
@abstractmethod
def build_edge(self, edge: Edge) -> etree.Element:
pass
@abstractmethod
def post_modify(self, content: str) -> str:
pass
class KlskBuilder(Builder):
def __init__(self, _: list[Node]):
pass
@override
def nsmap(self) -> dict[str | None, str]:
return {
None: 'http://graphml.graphdrawing.org/xmlns',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
}
@override
def build_keys(self) -> list[etree.Element]:
k_node = etree.Element('key', attrib={
'id': 'code',
'for': 'node',
'attr.name': 'code',
'attr.type': 'string'
})
k_step = etree.Element('key', attrib={
'id': 'step',
'for': 'node',
'attr.name': 'step',
'attr.type': 'int'
})
return [k_node, k_step]
@override
def build_node(self, node: Node) -> etree.Element:
node_xml = etree.Element('node', id=node.id)
etree.SubElement(node_xml, 'data', key='code').text = node.code
etree.SubElement(node_xml, 'data', key='step').text = str(node.step)
return node_xml
@override
def build_edge(self, edge: Edge) -> etree.Element:
return etree.Element('edge', source=edge.src, target=edge.dst)
@override
def post_modify(self, content: str) -> str:
return content
class yEdBuilder(KlskBuilder):
def __build_colors(self, num: int) -> list[str]:
cmap = mcolors.LinearSegmentedColormap.from_list('custom_bwr', ['#0000ff', '#e8daef', '#ff0000'])
return [mcolors.to_hex(cmap(x)) for x in np.linspace(0, 1, num)]
def __init__(self, nodes: list[Node]):
max_step = max(x.step for x in nodes)
self.__colors = self.__build_colors(max_step + 1)
self.__yed_ns = 'http://www.yworks.com/xml/graphml'
@override
def nsmap(self) -> dict[str | None, str]:
return {
**super().nsmap(),
'y': self.__yed_ns
}
@override
def build_keys(self) -> list[etree.Element]:
k_info = etree.Element('key', attrib={
'id': 'info',
'for': 'node',
'yfiles.type': 'nodegraphics'
})
return super().build_keys() + [k_info]
@override
def build_node(self, node: Node) -> etree.Element:
shape = etree.Element(f'{{{self.__yed_ns}}}ShapeNode')
etree.SubElement(shape, f'{{{self.__yed_ns}}}Fill', attrib={'color': self.__colors[node.step]})
etree.SubElement(shape, f'{{{self.__yed_ns}}}Shape', attrib={'type': 'ellipse'})
etree.SubElement(shape, f'{{{self.__yed_ns}}}Geometry', attrib={'height': '50', 'width': '50'})
label = etree.SubElement(shape, f'{{{self.__yed_ns}}}NodeLabel', attrib={'fontSize': '10', 'modelName': 'internal'})
label.text = f'{node.code}
({node.step})'
node_xml = super().build_node(node)
node_info = etree.SubElement(node_xml, 'data', attrib={'key': 'info'})
node_info.append(shape)
return node_xml
@override
def post_modify(self, content: str) -> str:
return content.replace('
', '
')
class GraphML:
@dataclass(frozen=True) @dataclass(frozen=True)
class Graph: class Graph:
name: str name: str
nodes: list[GraphML.Node] nodes: list[Node]
edges: list[GraphML.Edge] edges: list[Edge]
@staticmethod @staticmethod
def __load(name: str, graph: ig.Graph) -> GraphML.Graph: def __load_graph(name: str, graph: ig.Graph) -> GraphML.Graph:
nodes = [] nodes = []
id_len = len(str(graph.vcount() - 1)) id_len = len(str(graph.vcount() - 1))
for index in range(graph.vcount()): for index in range(graph.vcount()):
info = graph.vs[index] info = graph.vs[index]
nodes.append(GraphML.Node(f'n{index:0{id_len}d}', info['code'], info['step'])) nodes.append(Node(f'n{index:0{id_len}d}', info['code'], info['step']))
edges = [] edges = []
for n1, n2 in graph.get_edgelist(): for n1, n2 in graph.get_edgelist():
@ -41,74 +162,51 @@ class GraphML:
node_2 = nodes[n2] node_2 = nodes[n2]
if node_1.step < node_2.step: if node_1.step < node_2.step:
node_1, node_2 = node_2, node_1 node_1, node_2 = node_2, node_1
edges.append(GraphML.Edge(node_1.id, node_2.id)) edges.append(Edge(node_1.id, node_2.id))
return GraphML.Graph(name, nodes, edges) return GraphML.Graph(name, nodes, edges)
def __init__(self, *graphs: tuple[str, ig.Graph]): def __init__(self, tag: str, graph: ig.Graph):
self.__graphs = [self.__load(*x) for x in graphs] self.__graph = self.__load_graph(tag, graph)
def __dump_node(self, node: Node) -> etree.Element:
node_xml = etree.Element('node', id=node.id)
etree.SubElement(node_xml, 'data', key='v_code').text = node.code
etree.SubElement(node_xml, 'data', key='v_step').text = str(node.step)
return node_xml
def __dump_edge(self, edge: Edge) -> etree.Element:
return etree.Element('edge', source=edge.src, target=edge.dst)
def __dump_graph(self, graph: Graph) -> etree.Element: def __dump_graph(self, graph: Graph, builder: Builder) -> etree.Element:
graph_xml = etree.Element('graph', id=graph.name, edgedefault='undirected') graph_xml = etree.Element('graph', id=graph.name, edgedefault='undirected')
for node in graph.nodes: for node in graph.nodes:
graph_xml.append(self.__dump_node(node)) graph_xml.append(builder.build_node(node))
for edge in graph.edges: for edge in graph.edges:
graph_xml.append(self.__dump_edge(edge)) graph_xml.append(builder.build_edge(edge))
return graph_xml return graph_xml
def save_graph(self, output: str) -> None: def save_graph(self, output: str, builder_t: type[Builder]) -> None:
graphml = etree.Element('graphml', nsmap={ builder = builder_t(self.__graph.nodes)
None: 'http://graphml.graphdrawing.org/xmlns',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance' graphml = etree.Element('graphml', nsmap=builder.nsmap())
})
graphml.set( graphml.set(
'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation', '{http://www.w3.org/2001/XMLSchema-instance}schemaLocation',
'http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd' 'http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd'
) )
etree.SubElement(graphml, 'key', attrib={ [graphml.append(x) for x in builder.build_keys()]
'id': 'v_code', graphml.append(self.__dump_graph(self.__graph, builder))
'for': 'node',
'attr.name': 'code',
'attr.type': 'string'
})
etree.SubElement(graphml, 'key', attrib={
'id': 'v_step',
'for': 'node',
'attr.name': 'step',
'attr.type': 'int'
})
for graph in self.__graphs: fake_output = io.BytesIO()
graphml.append(self.__dump_graph(graph))
xml_tree = etree.ElementTree(graphml) xml_tree = etree.ElementTree(graphml)
xml_tree.write(output, pretty_print=True, xml_declaration=True, encoding='utf-8') xml_tree.write(fake_output, pretty_print=True, xml_declaration=True, encoding='utf-8')
content = fake_output.getvalue().decode('utf-8')
with open(output, 'w') as fp:
def to_graphml(inputs: list[tuple[str, str]], output: str) -> None: fp.write(builder.post_modify(content))
print(f'Convert into {output}')
gml = GraphML(*((x, ig.Graph.Read_Pickle(y)) for x, y in inputs))
gml.save_graph(output)
def convert_graphs(input_dir: str, output_dir: str) -> None:
files = [x.removesuffix('.pkl') for x in os.listdir(input_dir) if x.endswith('.pkl')]
tags = sorted(x for x in files if re.match(r'^\d\-\d\d(L|R|M)$', x))
for tag in tags: def to_graphml(tag: str, input: str, output: str, is_yed: bool) -> None:
output_file = os.path.join(output_dir, f'{tag}.graphml') print(f'Convert graph {input} into {output}')
pkls = sorted(x for x in files if x.startswith(tag)) gml = GraphML(tag, ig.Graph.Read_Pickle(input))
to_graphml([(x, os.path.join(input_dir, f'{x}.pkl')) for x in pkls], output_file) gml.save_graph(output, yEdBuilder if is_yed else KlskBuilder)
if __name__ == "__main__": if __name__ == '__main__':
os.makedirs('output-gml', exist_ok=True) os.makedirs('output-gml', exist_ok=True)
convert_graphs('output-ig', 'output-gml') os.makedirs('output-yed', exist_ok=True)
for name in sorted(os.listdir('output-ig')):
name = name.removesuffix('.pkl')
tag = name.split('_')[0] if '_' in name else name
to_graphml(tag, f'output-ig/{name}.pkl', f'output-gml/{name}.graphml', False)
to_graphml(tag, f'output-ig/{name}.pkl', f'output-yed/{name}.graphml', True)

Loading…
Cancel
Save