Search code examples
pythonmatplotlibpowerbivisualizationorgchart

Python or PBI visualization - condensed tree chart - how to achieve and how the visual is properly called?


Maybe a bit odd and random question but still.

A little while back I have seen the below visual in one webinar, which visualizes the org strcture of an organization (who reports to whom etc):

enter image description here

As you see it basically shows that CEO is at the top of organization, Manufacturing director, Finance Director and Operations director are reporting to CEO etc. Each box length shows how many total children each node has (so for CEO it is all people in org, for Manufacturing director it is all people who report to him directly or indirectly etc). The boxes are color coded based on location where the position is located (so like red - USA, Blue - APAC etc)

Now, I love the visual itself, it is very nicely communicates in condensed way the whole org chart.

I want to replicate this chart using my own dataset, but I am not sure even where to start since I have no idea how such chart type is even called. I tried to google it or look through different chart libraries online, but haven't found anything similar.

Hence I have two questions:

  1. Does anyone knows how such chart is properly called?
  2. Even if you don't know how it is called, did anyone ever had a chance to do something similar, ideally in PowerBI or Python?

To illustrate, my dummy dataset is as follows:

enter image description here

Any advice will be much appreciated.


Solution

  • Given the x, y position, width and height of a manager rectangle, it is pretty straightforward to compute the positions and shapes of the people directly managed by that person. Since the the "Reporting to" relation defines a directed acyclic graph, we can start at the root and recursively traverse the graph to compute the position and shapes of all rectangles. This ensures that we always compute the rectangle for a manager before computing the rectangle for any of the people managed by that person.

    enter image description here

    #!/usr/bin/env python
    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    import networkx as nx
    
    
    def get_layout_recursively(G, predecessor, layout, base_width, base_height):
        x, y, _, _ = layout[predecessor]
        y -= base_height
        for node in G.successors(predecessor):
            width = get_total_children(G, node) * base_width
            layout[node] = x, y, width, base_height
            x += width
            layout = get_layout_recursively(G, node, layout, base_width, base_height)
        return layout
    
    
    def get_total_children(G, node):
        return len(nx.nodes(nx.dfs_tree(G, node)))
    
    
    if __name__ == '__main__':
    
        data = dict(
            ID          = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
            predecessor = [pd.NA, 1, 1, 2, 2, 4, 4, 4, 3, 3],
            label       = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
            color       = ['tab:blue', 'tab:orange', 'tab:green', 'tab:orange', 'tab:orange', 'tab:pink', 'tab:pink', 'tab:pink', 'tab:green', 'tab:green'],
        )
        data = pd.DataFrame(data)
        data.set_index('ID', inplace=True)
    
        # create graph 
        G = nx.DiGraph([(predecessor, successor) for (successor, predecessor) in data['predecessor'].iteritems() if not predecessor is pd.NA])
    
        # compute layout recursively starting with the root
        base_width = 2
        base_height = 10
        root = next(nx.topological_sort(G))
        layout = {root : (0, 0, get_total_children(G, root) * base_width, base_height)}
        layout = get_layout_recursively(G, root, layout, base_width, base_height)
        
        # plot        
        fig, ax = plt.subplots()
        for node in data.index:
            x, y, width, height = layout[node]
            color = data['color'].loc[node]
            label = data['label'].loc[node]
            ax.add_patch(plt.Rectangle((x, y), width, height, facecolor=color, edgecolor='white'))
            ax.text(x + 0.5*width, y + 0.5 * height, label, ha='center', va='center', color='white')
    
        # rescale the axis such that it encompasses all rectangles
        ax.autoscale_view()
    
        # remove axes spines, ticks, labels
        ax.axis('off')
    
        plt.show()