pythonpdfcairohierarchicaltreetable

How to print tree table visualization in PDF use Cairo and Python?


Following the question:

Tree plotting in Python

The author's web site

I want to visualize the tree table (hierarchical structure) in PDF using Cairo and Python.

I have modified the code as follows:

import uuid
import cairo


def sanitize_id(id):
    return id.strip().replace(" ", "")


(_ADD, _DELETE, _INSERT) = range(3)
(_ROOT, _DEPTH, _WIDTH) = range(3)


class Node:

    def __init__(self, name, identifier=None, expanded=True):
    self.__identifier = (str(uuid.uuid1()) if identifier is None else
                         sanitize_id(str(identifier)))
    self.name = name
    self.expanded = expanded
    self.__bpointer = None
    self.__fpointer = []

    @property
    def identifier(self):
    return self.__identifier

    @property
    def bpointer(self):
    return self.__bpointer

    @bpointer.setter
    def bpointer(self, value):
    if value is not None:
        self.__bpointer = sanitize_id(value)

    @property
    def fpointer(self):
    return self.__fpointer

    def update_fpointer(self, identifier, mode=_ADD):
    if mode is _ADD:
        self.__fpointer.append(sanitize_id(identifier))
    elif mode is _DELETE:
        self.__fpointer.remove(sanitize_id(identifier))
    elif mode is _INSERT:
        self.__fpointer = [sanitize_id(identifier)]


class Tree(object):

    def __init__(self, cr):
    self._context = cr
    self._colx = 50.0
    self._coly = 50.0
    self.textW = 128.0
    self.textH = 20.0
    self.nodes = []

    def get_index(self, position):
    for index, node in enumerate(self.nodes):
        if node.identifier == position:
            break
    return index

    def create_node(self, name, identifier=None, parent=None):

    node = Node(name, identifier)
    self.nodes.append(node)
    self.__update_fpointer(parent, node.identifier, _ADD)
    node.bpointer = parent
    return node

    def ShowText(self, x, y, st):

    self._context.move_to(x, y)
    self._context.show_text(st)
    self._context.stroke()

    def ShowRectText(self, x, y, w, h, st):
    self.ShowText(x, y, st)
    self._context.rectangle(x - 5, y - self.textH, w, h)
    self._context.stroke()

    def show(self, position, level=_ROOT):
    queue = self[position].fpointer
    h = self.textH*self.__len__()

    if level == _ROOT:
        s1 = "{0} [{1}]".format(self[position].name,
                                self[position].identifier)

        self.ShowRectText(self._colx, self._coly, self.textW, h, s1)
        self._coly = self._coly + self.textH

    else:
        s2 = "{0} [{1}]".format(self[position].name, self[position].identifier)
        self._colx = self._colx + self.textW * level
        self.ShowRectText(self._colx, self._coly, self.textW, h, s2)
        self._coly = self._coly + self.textH
        self._colx = self._colx - self.textW * level
        self._context.stroke()

    if self[position].expanded:
        level += 1
        for element in queue:
            self.show(element, level)  # recursive call

    def expand_tree(self, position, mode=_DEPTH):
    # Python generator. Loosly based on an algorithm from 'Essential LISP' by
    # John R. Anderson, Albert T. Corbett, and Brian J. Reiser, page 239-241
    yield position
    queue = self[position].fpointer
    while queue:
        yield queue[0]
        expansion = self[queue[0]].fpointer
        if mode is _DEPTH:
            queue = expansion + queue[1:]  # depth-first
        elif mode is _WIDTH:
            queue = queue[1:] + expansion  # width-first

    def is_branch(self, position):
    return self[position].fpointer

    def __update_fpointer(self, position, identifier, mode):
    if position is None:
        return
    else:
        self[position].update_fpointer(identifier, mode)

    def __update_bpointer(self, position, identifier):
    self[position].bpointer = identifier

    def __getitem__(self, key):
    return self.nodes[self.get_index(key)]

    def __setitem__(self, key, item):
    self.nodes[self.get_index(key)] = item

    def __len__(self):
    return len(self.nodes)

    def __contains__(self, identifier):
    return [node.identifier for node in self.nodes
            if node.identifier is identifier]


if __name__ == "__main__":
    surface = cairo.PDFSurface("cairo_tree_table_show.pdf", 1000, 800)
    context = cairo.Context(surface)
    tree = Tree(context)

    tree.create_node("Harry", "harry")  # root node
    tree.create_node("Jane", "jane", parent="harry")
    tree.create_node("Bill", "bill", parent="harry")
    tree.create_node("Joe", "joe", parent="jane")
    tree.create_node("Diane", "diane", parent="jane")
    tree.create_node("George", "george", parent="diane")
    tree.create_node("Mary", "mary", parent="diane")
    tree.create_node("Jill", "jill", parent="george")
    tree.create_node("Carol", "carol", parent="jill")
    tree.create_node("Grace", "grace", parent="bill")
    tree.create_node("Mark", "mark", parent="jane")

    tree.show("harry")

It gives me this:

result

but I want as:

expected

If I can get level number of the tree leaf in the loop,I set the rectangle height = "(this level's leaf)*textH" ,draw the table.


Solution

  • I have added some functions (get_leaf_nodes) to get all the leaf and computing the height of current grid.

    reference this question from Alvaro Fuentes

    The grid height is right now.I have to say that Stack Overflow is great.Always give me what my want.

    And result is :

    result

    import uuid
    import cairo
    
    
    def sanitize_id(id):
        return id.strip().replace(" ", "")
    
    
    (_ADD, _DELETE, _INSERT) = range(3)
    (_ROOT, _DEPTH, _WIDTH) = range(3)
    
    
    class Node:
    
        def __init__(self, name, identifier=None, expanded=True):
            self.__identifier = (str(uuid.uuid1()) if identifier is None else
                                 sanitize_id(str(identifier)))
            self.name = name
            self.expanded = expanded
            self.__bpointer = None
            self.__fpointer = []
    
        @property
        def identifier(self):
            return self.__identifier
    
        @property
        def bpointer(self):
            return self.__bpointer
    
        @bpointer.setter
        def bpointer(self, value):
            if value is not None:
                self.__bpointer = sanitize_id(value)
    
        @property
        def fpointer(self):
            return self.__fpointer
    
        def update_fpointer(self, identifier, mode=_ADD):
            if mode is _ADD:
                self.__fpointer.append(sanitize_id(identifier))
            elif mode is _DELETE:
                self.__fpointer.remove(sanitize_id(identifier))
            elif mode is _INSERT:
                self.__fpointer = [sanitize_id(identifier)]
    
    
    class Tree(object):
    
        def __init__(self, cr):
            self._context = cr
            self._colx = 50.0
            self._coly = 50.0
            self.textW = 128.0
            self.textH = 20.0
            self.leafs = []
            self.nodes = []
    
        def get_leaf_nodes(self, position):
            """get all leafs"""
            self.leafs = []
            self._collect_leaf_nodes(position)
            return self.leafs
    
        def _collect_leaf_nodes(self, position):
            queue = self[position].fpointer
            if queue == []:
                self.leafs.append(self[position])
            else:
                for n in queue:
                    self._collect_leaf_nodes(n)
    
        def get_index(self, position):
            for index, node in enumerate(self.nodes):
                if node.identifier == position:
                    break
            return index
    
        def create_node(self, name, identifier=None, parent=None):
    
            node = Node(name, identifier)
            self.nodes.append(node)
            self.__update_fpointer(parent, node.identifier, _ADD)
            node.bpointer = parent
            return node
    
        def ShowText(self, x, y, st):
    
            self._context.move_to(x, y)
            self._context.show_text(st)
            self._context.stroke()
    
        def ShowRectText(self, x, y, w, h, st):
            self.ShowText(x, y, st)
            self._context.rectangle(x - 5, y - 0.8 * self.textH, w, h)
            self._context.stroke()
    
        def show(self, position, level=_ROOT):
            queue = self[position].fpointer
            # get all the children
            h = self.textH * len(self.get_leaf_nodes(position))
            if level == _ROOT:
                s1 = "{0} [{1}]".format(self[position].name,
                                    self[position].identifier)
                self.ShowRectText(self._colx, self._coly, self.textW, h, s1)
    
            else:
                s2 = "{0} [{1}]".format(self[position].name, self[position].identifier)
                self._colx = self._colx + self.textW * level
    
                self.ShowRectText(self._colx, self._coly, self.textW, h, s2)
                if queue==[]:
                    self._coly = self._coly + self.textH
                self._colx = self._colx - self.textW * level
    
            if self[position].expanded:
                level += 1
                for element in queue:
                    self.show(element, level)  # recursive call
    
        def expand_tree(self, position, mode=_DEPTH):
        # Python generator. Loosly based on an algorithm from 'Essential LISP' by
        # John R. Anderson, Albert T. Corbett, and Brian J. Reiser, page 239-241
        # http://www.quesucede.com/page/show/id/python-3-tree-implementation
            yield position
            queue = self[position].fpointer
            while queue:
                yield queue[0]
                expansion = self[queue[0]].fpointer
                if mode is _DEPTH:
                    queue = expansion + queue[1:]  # depth-first
                elif mode is _WIDTH:
                    queue = queue[1:] + expansion  # width-first
    
        def is_branch(self, position):
            return self[position].fpointer
    
        def __update_fpointer(self, position, identifier, mode):
            if position is None:
                return
            else:
                self[position].update_fpointer(identifier, mode)
    
        def __update_bpointer(self, position, identifier):
            self[position].bpointer = identifier
    
        def __getitem__(self, key):
            return self.nodes[self.get_index(key)]
    
        def __setitem__(self, key, item):
            self.nodes[self.get_index(key)] = item
    
        def __len__(self):
            return len(self.nodes)
    
        def __contains__(self, identifier):
            return [node.identifier for node in self.nodes
                    if node.identifier is identifier]
    
    
    if __name__ == "__main__":
        surface = cairo.PDFSurface("cairo_tree_table_show.pdf", 1000, 800)
        context = cairo.Context(surface)
        tree = Tree(context)
    
        tree.create_node("Harry", "harry")  # root node
        tree.create_node("Jane", "jane", parent="harry")
        tree.create_node("Bill", "bill", parent="harry")
        tree.create_node("Joe", "joe", parent="jane")
        tree.create_node("Diane", "diane", parent="jane")
        tree.create_node("George", "george", parent="diane")
        tree.create_node("Mary", "mary", parent="diane")
        tree.create_node("Jill", "jill", parent="george")
        tree.create_node("Carol", "carol", parent="jill")
        tree.create_node("Grace", "grace", parent="bill")
        tree.create_node("Mark", "mark", parent="jane")
        tree.show("harry")