_graphviz.py 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. # yamify - define family trees in YAML
  2. #
  3. # Copyright (C) 2020 Fabian Peter Hammerle <fabian@hammerle.me>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import typing
  18. import graphviz
  19. from yamily import Person, PersonCollection
  20. def _add_person_node(graph: graphviz.dot.Digraph, person: Person) -> None:
  21. label = person.name or person.identifier
  22. if person.birth_date is not None:
  23. label += r"\n*{}".format(person.birth_date.isoformat())
  24. if person.death_date is not None:
  25. label += r"\n†{}".format(person.death_date.isoformat())
  26. graph.node(person.identifier, label=label, shape="box")
  27. def _add_parent_node(graph: graphviz.dot.Digraph, parents: typing.Set[Person]) -> str:
  28. parent_node_name = "relation-{}-{}".format(
  29. parents[0].identifier, parents[1].identifier
  30. )
  31. graph.node(parent_node_name, shape="point", width="0")
  32. for parent in parents:
  33. graph.edge(
  34. parent.identifier, parent_node_name, constraint="False", arrowhead="none",
  35. )
  36. return parent_node_name
  37. def digraph(collection: PersonCollection) -> graphviz.dot.Digraph:
  38. """
  39. >>> bob = Person('bob')
  40. >>> bob.father = Person('frank')
  41. >>> carol = Person('carol')
  42. >>> carol.mother = Person('grace')
  43. >>> alice = Person('alice')
  44. >>> alice.mother = carol
  45. >>> alice.father = bob
  46. >>> collection = PersonCollection()
  47. >>> collection.add_person(alice)
  48. Person(alice)
  49. >>> graph = digraph(collection)
  50. >>> graph.render("/tmp/tree.dot")
  51. '/tmp/tree.dot.pdf'
  52. """
  53. graph = graphviz.Digraph("yamily")
  54. nodes: typing.Set[Person] = set()
  55. parent_node_names: typing.Dict[typing.Tuple[Person, Person], str] = dict()
  56. for person in collection:
  57. if person in nodes:
  58. continue
  59. # https://graphviz.gitlab.io/_pages/Gallery/directed/cluster.html
  60. with graph.subgraph(name="cluster_" + person.identifier) as subgraph:
  61. subgraph.attr(rank="same", style="invisible")
  62. _add_person_node(subgraph, person)
  63. nodes.add(person)
  64. for coparent in collection.get_coparents(person):
  65. parents = tuple(sorted((person, coparent), key=lambda p: p.identifier))
  66. parent_node_name = _add_parent_node(subgraph, parents)
  67. parent_node_names[parents] = parent_node_name
  68. parent_node_names[tuple(parents[::-1])] = parent_node_name
  69. _add_person_node(subgraph, coparent)
  70. nodes.add(coparent)
  71. for child in collection:
  72. if child.mother is not None and child.father is not None:
  73. parents = sorted(child.parents, key=lambda p: p.identifier)
  74. parent_node_name = parent_node_names[tuple(parents)]
  75. graph.edge(parent_node_name, child.identifier)
  76. elif child.mother is not None:
  77. graph.edge(child.mother.identifier, child.identifier)
  78. elif child.father is not None:
  79. graph.edge(child.father.identifier, child.identifier)
  80. return graph