Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to create graphs with duplicate nodes #53

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 154 additions & 28 deletions pyyed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,45 @@ def __init__(self, group_id, parent_graph, label=None, label_alignment="center",
closed="false", font_family="Dialog", underlined_text="false",
font_style="plain", font_size="12", fill="#FFCC00", transparent="false",
border_color="#000000", border_type="line", border_width="1.0", height=False,
width=False, x=False, y=False, custom_properties=None, description="", url=""):
width=False, x=False, y=False, custom_properties=None, description="", url="", node_id=None):
"""

:param group_id:
:param parent_graph:
:param label:
:param label_alignment:
:param shape:
:param closed:
:param font_family:
:param underlined_text:
:param font_style:
:param font_size:
:param fill:
:param transparent:
:param border_color:
:param border_type:
:param border_width:
:param height:
:param width:
:param x:
:param y:
:param custom_properties:
:param description:
:param url:
:param node_id: If set, will allow a different name than the node_name (to allow duplicates)
"""
self.label = label
if label is None:
self.label = group_id

self.parent = None
self.group_id = group_id

if node_id is not None:
self.node_id = node_id
else:
self.node_id = group_id

self.nodes = {}
self.groups = {}
self.parent_graph = parent_graph
Expand Down Expand Up @@ -232,23 +263,31 @@ def __init__(self, group_id, parent_graph, label=None, label_alignment="center",
setattr(self, name, definition.default_value)

def add_node(self, node_name, **kwargs):
if node_name in self.parent_graph.existing_entities:
raise RuntimeWarning("Node %s already exists" % node_name)
if self.parent_graph.duplicates:
node_id = self.parent_graph._next_unique_identifier()
else:
if node_name in self.parent_graph.existing_entities:
raise RuntimeWarning("Node %s already exists" % node_name)
node_id = node_name

node = Node(node_name, **kwargs)
node = Node(node_name, node_id=node_id, **kwargs)
node.parent = self
self.nodes[node_name] = node
self.parent_graph.existing_entities[node_name] = node
self.nodes[node_id] = node
self.parent_graph.existing_entities[node_id] = node
return node

def add_group(self, group_id, **kwargs):
if group_id in self.parent_graph.existing_entities:
raise RuntimeWarning("Node %s already exists" % group_id)
if self.parent_graph.duplicates:
node_id = self.parent_graph._next_unique_identifier()
else:
if group_id in self.parent_graph.existing_entities:
raise RuntimeWarning("Node %s already exists" % group_id)
node_id = group_id

group = Group(group_id, self.parent_graph, **kwargs)
group = Group(group_id, self.parent_graph, node_id=node_id, allow_duplicates=self.duplicates, **kwargs)
group.parent = self
self.groups[group_id] = group
self.parent_graph.existing_entities[group_id] = group
self.groups[node_id] = group
self.parent_graph.existing_entities[node_id] = group
return group

def is_ancestor(self, node):
Expand Down Expand Up @@ -277,8 +316,25 @@ def add_edge(self, node1_name, node2_name, **kwargs):
self.edges[edge.edge_id] = edge
return edge

def add_edge_by_obj(self, node1, node2, **kwargs):
# pass node names, not actual node objects

# http://graphml.graphdrawing.org/primer/graphml-primer.html#Nested
# The edges between two nodes in a nested graph have to be declared in a graph,
# which is an ancestor of both nodes in the hierarchy.

if not (self.is_ancestor(node1) and self.is_ancestor(node2)):
raise RuntimeWarning("Group %s is not ancestor of both %s and %s" % (self.group_id, node1.node_name,
node2.node_name))

self.parent_graph.num_edges += 1
kwargs['edge_id'] = str(self.parent_graph.num_edges)
edge = Edge(node1.node_id, node2.node_id, **kwargs)
self.edges[edge.edge_id] = edge
return edge

def convert(self):
node = ET.Element("node", id=self.group_id)
node = ET.Element("node", id=self.node_id)
node.set("yfiles.foldertype", "group")
data = ET.SubElement(node, "data", key="data_node")

Expand Down Expand Up @@ -352,9 +408,35 @@ def __init__(self, node_name, label=None, label_alignment="center", shape="recta
shape_fill="#FF0000", transparent="false", border_color="#000000",
border_type="line", border_width="1.0", height=False, width=False, x=False,
y=False, node_type="ShapeNode", UML=False,
custom_properties=None, description="", url=""):
custom_properties=None, description="", url="", node_id=None):
"""

self.list_of_labels = [] # initialize list of labels
:param node_name:
:param label:
:param label_alignment:
:param shape:
:param font_family:
:param underlined_text:
:param font_style:
:param font_size:
:param shape_fill:
:param transparent:
:param border_color:
:param border_type:
:param border_width:
:param height:
:param width:
:param x:
:param y:
:param node_type:
:param UML:
:param custom_properties:
:param description:
:param url:
:param node_id: If set, will allow a different name than the node_name (to allow duplicates)
"""

self.list_of_labels = [] # initialize list of labels
if label:
self.add_label(label, alignment=label_alignment,
font_family = font_family, underlined_text = underlined_text,
Expand All @@ -364,7 +446,12 @@ def __init__(self, node_name, label=None, label_alignment="center", shape="recta
font_family = font_family, underlined_text = underlined_text,
font_style = font_style, font_size = font_size)

self.node_name = node_name
self.node_name = node_name

if node_id is not None:
self.node_id = node_id
else:
self.node_id = node_name

self.node_type = node_type
self.UML = UML
Expand Down Expand Up @@ -421,7 +508,7 @@ def add_label(self, label_text, **kwargs):

def convert(self):

node = ET.Element("node", id=str(self.node_name))
node = ET.Element("node", id=str(self.node_id))
data = ET.SubElement(node, "data", key="data_node")
shape = ET.SubElement(data, "y:" + self.node_type)

Expand All @@ -441,7 +528,7 @@ def convert(self):
ET.SubElement(shape, "y:Shape", type=self.shape)

if self.UML:
UML = ET.SubElement(shape, "y:UML")
UML = ET.SubElement(shape, "y:UML", use3DEffect="false")

attributes = ET.SubElement(UML, "y:AttributeLabel", type=self.shape)
attributes.text = self.UML["attributes"]
Expand Down Expand Up @@ -573,11 +660,22 @@ def set_custom_properties_defs(cls, custom_property):


class Graph:
def __init__(self, directed="directed", graph_id="G"):
def __init__(self, directed="directed", graph_id="G", allow_duplicates=False):
"""

:param directed:
:param graph_id:
:param allow_duplicates: False by default to keep compatibility with past behavior. If True, text in node
will be different than label to ensure we can add multiple nodes with the same name.
"""

self.nodes = {}
self.edges = {}
self.num_edges = 0
self.duplicates = allow_duplicates

# Only used if duplicates = True
self.num_nodes = 0

self.directed = directed
self.graph_id = graph_id
Expand Down Expand Up @@ -674,12 +772,17 @@ def get_graph(self):
return ET.tostring(self.graphml, encoding='UTF-8').decode()

def add_node(self, node_name, **kwargs):
if node_name in self.existing_entities:
raise RuntimeWarning("Node %s already exists" % node_name)

node = Node(node_name, **kwargs)
self.nodes[node_name] = node
self.existing_entities[node_name] = node
if self.duplicates:
node_id = self._next_unique_identifier()
else:
if node_name in self.existing_entities:
raise RuntimeWarning("Node %s already exists" % node_name)
node_id = node_name

node = Node(node_name, node_id=node_id, **kwargs)
self.nodes[node_id] = node
self.existing_entities[node_id] = node
return node

def add_edge(self, node1_name, node2_name, **kwargs):
Expand All @@ -694,13 +797,27 @@ def add_edge(self, node1_name, node2_name, **kwargs):
self.edges[edge.edge_id] = edge
return edge

def add_edge_by_obj(self, node1, node2, **kwargs):
# pass node names, not actual node objects


self.num_edges += 1
kwargs['edge_id'] = str(self.num_edges)
edge = Edge(node1.node_id, node2.node_id, **kwargs)
self.edges[edge.edge_id] = edge
return edge

def add_group(self, group_id, **kwargs):
if group_id in self.existing_entities:
raise RuntimeWarning("Node %s already exists" % group_id)
if self.duplicates:
node_id = self._next_unique_identifier()
else:
if group_id in self.existing_entities:
raise RuntimeWarning("Node %s already exists" % group_id)
node_id = group_id

group = Group(group_id, self, **kwargs)
self.groups[group_id] = group
self.existing_entities[group_id] = group
group = Group(group_id, self, node_id=node_id, **kwargs)
self.groups[node_id] = group
self.existing_entities[node_id] = group
return group

def define_custom_property(self, scope, name, property_type, default_value):
Expand All @@ -716,3 +833,12 @@ def define_custom_property(self, scope, name, property_type, default_value):
Node.set_custom_properties_defs(custom_property)
elif scope == "edge":
Edge.set_custom_properties_defs(custom_property)

def _next_unique_identifier(self):
"""
Increment internal counter, then return next identifier not yet used.
"""
self.num_nodes += 1
node_id = str(self.num_nodes)

return node_id