mirror of
ssh://git.janware.com/srv/git/janware/proj/jw-python
synced 2026-01-15 09:53:32 +01:00
graph.yed.MapAttr2Shape: Add Class
MapAttr2Shape is a class which allows processing GraphML files, such that nodes get added shape attributes understood by the yEd editor, based on the values of other node attributes. This allows to programmatically visualize node attributes. Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
parent
b2b726b632
commit
89578b788f
3 changed files with 199 additions and 0 deletions
4
tools/python/jwutils/graph/yed/Makefile
Normal file
4
tools/python/jwutils/graph/yed/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
TOPDIR = ../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-mod.mk
|
||||
191
tools/python/jwutils/graph/yed/MapAttr2Shape.py
Normal file
191
tools/python/jwutils/graph/yed/MapAttr2Shape.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from jwutils.log import *
|
||||
|
||||
class MapAttr2Shape: # export
|
||||
|
||||
def __init__(self, mappings: dict[str, str|Callable[[dict[str, str]], str]]|None=None) -> None:
|
||||
self.__mappings = mappings if mappings is not None else {}
|
||||
self.__shape_node_key = 'd25'
|
||||
self.__ns_gml = "http://graphml.graphdrawing.org/xmlns"
|
||||
self.__ns = {
|
||||
# -- Standard GraphML
|
||||
"": self.__ns_gml,
|
||||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"xsi:schemaLocation": "http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd",
|
||||
|
||||
# -- YWorks GraphML
|
||||
"java": "http://www.yworks.com/xml/yfiles-common/1.0/java",
|
||||
"sys": "http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0",
|
||||
"x": "http://www.yworks.com/xml/yfiles-common/markup/2.0",
|
||||
"y": "http://www.yworks.com/xml/graphml",
|
||||
"yed": "http://www.yworks.com/xml/yed/3",
|
||||
}
|
||||
# https://stackoverflow.com/questions/4997848/
|
||||
for name, url in self.__ns.items():
|
||||
ET.register_namespace(name, url)
|
||||
|
||||
def __keys(self, root):
|
||||
ret: dict[str, str] = {}
|
||||
for el in root.findall('key', self.__ns):
|
||||
attr_name = el.get('attr.name')
|
||||
id = el.get('id')
|
||||
if attr_name is not None:
|
||||
ret[attr_name] = id
|
||||
return ret
|
||||
|
||||
def __value(self, node, key):
|
||||
data = node.find(f'data[@key="{key}"]', self.__ns)
|
||||
if data is None:
|
||||
return None
|
||||
return data.text
|
||||
|
||||
def __attribs(self, node, keys):
|
||||
ret: dict[str, str] = {}
|
||||
for name, key in keys.items():
|
||||
val = self.__value(node, key)
|
||||
if val is None:
|
||||
continue
|
||||
ret[name] = val
|
||||
return ret
|
||||
|
||||
def __add_key_nodegraphics(self, root):
|
||||
# <graphml><key for="node" id="d22" yfiles.type="nodegraphics"/></graphml>
|
||||
el = ET.Element('key')
|
||||
for attr, val in {
|
||||
'id': self.__shape_node_key,
|
||||
'for': 'node',
|
||||
'yfiles.type':'nodegraphics'
|
||||
}.items():
|
||||
el.set(attr, val)
|
||||
root.append(el)
|
||||
|
||||
def __massage_node(self, node, keys: dict[str, str]) -> None:
|
||||
|
||||
def __add(parent, d: dict[str, Any]):
|
||||
for tag, content in d.items():
|
||||
if tag.find('y:') != -1:
|
||||
ns, tag = tag.split(':')
|
||||
tag = '{' + self.__ns[ns] + '}' + tag
|
||||
attrib = content.get('a') or {}
|
||||
el = ET.Element(tag, attrib=attrib)
|
||||
text = content.get('t')
|
||||
if text is not None:
|
||||
el.text = text
|
||||
parent.append(el)
|
||||
children = content.get('c')
|
||||
if children is not None:
|
||||
__add(el, children)
|
||||
|
||||
default_values = {
|
||||
'color': '#FFCC00',
|
||||
'text': ''
|
||||
}
|
||||
values = {}
|
||||
|
||||
for key, default in default_values.items():
|
||||
mapping = self.__mappings.get(key)
|
||||
if mapping is None:
|
||||
values[key] = default
|
||||
continue
|
||||
try:
|
||||
if isinstance(str, mapping):
|
||||
values[key] = mapping
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
mapped = mapping(self.__attribs(node, keys))
|
||||
values[key] = mapped or default
|
||||
|
||||
color = values['color']
|
||||
text = values['text']
|
||||
has_text = 'true' if text else 'false'
|
||||
|
||||
width_text = round(len(text) * 5.75, 5) if text else 0
|
||||
width_box = width_text + 10 if text else 30
|
||||
|
||||
shape = {
|
||||
'data': {
|
||||
'a': {'key': self.__shape_node_key},
|
||||
'c': {
|
||||
'y:ShapeNode': {
|
||||
'a': {},
|
||||
'c': {
|
||||
'y:Geometry': {'a': {'height': '30.0', 'width': str(width_box), 'x': str(-(width_box / 2)), 'y':' -15.0'}},
|
||||
'y:Fill': {'a': {'color': color, 'transparent': 'false'}},
|
||||
'y:BorderStyle': {'a': {'color': '#000000', 'raised': 'false', 'type': 'line', 'width': '1.0'}},
|
||||
'y:NodeLabel': {
|
||||
'a': {
|
||||
'alignment': 'center',
|
||||
'autoSizePolicy': 'content',
|
||||
'fontFamily': 'Dialog',
|
||||
'fontSize': '12',
|
||||
'fontStyle': 'plain',
|
||||
'hasBackgroundColor': 'false',
|
||||
'hasLineColor': 'false',
|
||||
'hasText': has_text,
|
||||
'height': '18',
|
||||
'horizontalTextPosition': 'center',
|
||||
'iconTextGap': '4',
|
||||
'modelName': 'custom',
|
||||
'textColor': '#000000',
|
||||
'verticalTextPosition': 'bottom',
|
||||
'visible': 'true',
|
||||
'width': str(width_text),
|
||||
'x': '13.0',
|
||||
'y': '13.0',
|
||||
},
|
||||
'c': {
|
||||
'y:LabelModel': {
|
||||
'c': {
|
||||
'y:SmartNodeLabelModel': {'a': {'distance': '4.0'}}
|
||||
},
|
||||
},
|
||||
'y:ModelParameter': {
|
||||
'c': {
|
||||
'y:SmartNodeLabelModelParameter': {
|
||||
'a': {
|
||||
'labelRatioX':'0.0',
|
||||
'labelRatioY': '0.0',
|
||||
'nodeRatioX': '0.0',
|
||||
'nodeRatioY': '0.0',
|
||||
'offsetX': '0.0',
|
||||
'offsetY': '0.0',
|
||||
'upX': '0.0',
|
||||
'upY': '-1.0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
't': text
|
||||
},
|
||||
'y:Shape': {'a': {'type': 'rectangle'}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__add(node, shape)
|
||||
|
||||
def __massage_nodes(self, root) -> None:
|
||||
keys = self.__keys(root)
|
||||
graph = root.find(f'graph', self.__ns)
|
||||
for node in graph:
|
||||
self.__massage_node(node, keys)
|
||||
|
||||
def run(self, path_in: str, path_out: str) -> None:
|
||||
parser = ET.XMLParser(encoding="utf-8")
|
||||
tree = ET.parse(path_in, parser=parser)
|
||||
root = tree.getroot()
|
||||
|
||||
self.__add_key_nodegraphics(root)
|
||||
self.__massage_nodes(root)
|
||||
|
||||
ET.indent(tree, space=' ', level=0)
|
||||
tree.write(path_out, xml_declaration=True, encoding='utf-8')
|
||||
Loading…
Add table
Add a link
Reference in a new issue