2 agosto, 2026

GENERAR ORGANIGRAMA CON AZURE FUNCTIONS, AUTOMATE Y POWER APPS

Hola a todos,

Este es uno de esos post que me gusta escribir porque la herramienta sobre que voy a hablar me ha encantado crearla. Se trata de la posibilidad de crear un organigrama desde Power Apps. Sí, uno del tipo de SmartArt, que permita ver en un control de imagen la representación de una estructura organizativa a través de un organigrama (en ese ejemplo, horizontal).

En primer lugar, he subido la estructura a una lista de SharePoint:

Con los campos Departamento, Nivel y Responsable. En el campo Nivel he detallado de forma en la que compongo la jerarquía del organigrama: la gerencia es nivel 1, de ella dependen las direcciones 2 y a su vez las direcciones de Área 3 y de estas las subdirecciones de Zona 4.

De esta forma, nuestro flujo de Automate va a enviar los datos para que la función de Python en VSC lea y genere el organigrama. Este es el código en VSC:

import json
import base64
from io import BytesIO

import azure.functions as func
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

# Config
BASE_BOX_WIDTH = 180
BOX_HEIGHT = 60
H_MARGIN = 40
V_MARGIN = 120
FONT_SIZE = 10
DPI = 100
FONT_FAMILY = "DejaVu Sans"

# Colores por nivel
LEVEL_COLORS = {
    1: "#1976D2",
    2: "#D32F2F",
    3: "#388E3C",
    4: "#7B1FA2",
}


# Medir texto
def measure_text_width(text, fontsize=FONT_SIZE):
    fig = plt.figure(figsize=(1, 1), dpi=100)
    txt = fig.text(0, 0, text, fontsize=fontsize, fontfamily=FONT_FAMILY)
    fig.canvas.draw()
    bbox = txt.get_window_extent(renderer=fig.canvas.get_renderer())
    plt.close(fig)
    return bbox.width


# Crear jerarquía
def build_smartart_tree(items):
    stack = []
    root = None

    for item in items:
        nivel = int(item["nivel"])
        node = {"data": item, "children": [], "level": nivel, "box_width": None}

        while len(stack) >= nivel:
            stack.pop()

        if stack:
            stack[-1]["children"].append(node)
        else:
            root = node

        stack.append(node)

    return root


# Ancho dinámico
def set_dynamic_box_widths(node):
    d = node["data"]
    dept_w = measure_text_width(d["departamento"])
    pers_w = measure_text_width(d["persona"])
    node["box_width"] = max(BASE_BOX_WIDTH, max(dept_w, pers_w) + 20)

    for c in node["children"]:
        set_dynamic_box_widths(c)


# Ancho total
def compute_widths(node):
    if not node["children"]:
        node["width"] = node["box_width"]
        return node["width"]

    total = sum(compute_widths(c) + H_MARGIN for c in node["children"]) - H_MARGIN
    node["width"] = max(node["box_width"], total)
    return node["width"]


# Profundidad
def compute_depth(node, level=1):
    if not node["children"]:
        return level
    return max(compute_depth(c, level + 1) for c in node["children"])


# Posiciones
def assign_coordinates(node, x_start=0, y=0):
    node["x"] = x_start + (node["width"] - node["box_width"]) / 2
    node["y"] = y

    cur_x = x_start
    for c in node["children"]:
        assign_coordinates(c, cur_x, y + V_MARGIN)
        cur_x += c["width"] + H_MARGIN


# Render PNG
def render_png(root):
    total_width = root["width"] + 2 * H_MARGIN
    depth = compute_depth(root)
    total_height = depth * V_MARGIN + BOX_HEIGHT + 2 * V_MARGIN

    fig, ax = plt.subplots(figsize=(total_width / DPI, total_height / DPI), dpi=DPI)

    def draw(n):
        d = n["data"]
        x = n["x"] + H_MARGIN
        y = n["y"] + V_MARGIN
        bw = n["box_width"]

        color = LEVEL_COLORS.get(n["level"], "#BDBDBD")

        rect = Rectangle((x, y), bw, BOX_HEIGHT, linewidth=1.3,
                         edgecolor="black", facecolor=color)
        ax.add_patch(rect)

        ax.text(x + 5, y + 20, d["departamento"], fontsize=FONT_SIZE,
                fontfamily=FONT_FAMILY, color="white")

        ax.text(x + 5, y + 40, d["persona"], fontsize=FONT_SIZE,
                fontfamily=FONT_FAMILY, color="yellow")

        for c in n["children"]:
            cx = c["x"] + H_MARGIN + c["box_width"] / 2
            cy = c["y"] + V_MARGIN
            ax.plot([x + bw/2, cx], [y + BOX_HEIGHT, cy],
                    linewidth=2, color="black")
            draw(c)

    draw(root)

    ax.set_xlim(0, total_width)
    ax.set_ylim(total_height, 0)
    ax.axis("off")

    fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)

    buf = BytesIO()
    fig.savefig(buf, format="png", bbox_inches=None, pad_inches=1.0)
    plt.close(fig)

    return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"


# Azure Function
def main(req: func.HttpRequest) -> func.HttpResponse:
    try:
        items = req.get_json()["empleados"]

        root = build_smartart_tree(items)
        set_dynamic_box_widths(root)
        compute_widths(root)
        assign_coordinates(root)

        png = render_png(root)
        return func.HttpResponse(png, mimetype="text/plain", status_code=200)

    except Exception as e:
        return func.HttpResponse(f"ERROR: {str(e)}", status_code=500)

y que necesitaremos desplegar en Azure:

Una vez que ya lo tenemos desplegado vamos a crear el flujo:

Y con esto si componemos nuestra App y en un botón de GENERAR ORGANIGRAMA en la propiedad OnSelect incluimos lo siguiente:

Set(
    varIMG;
    GENERARORGANIGRAMA.Run().imagenorganigrama
);;

Al pulsar vamos a obtener nuestro organigrama:

Además podemos incluir algunos controles para incluir scroll en el control html, en la propiedad HtmlText:

"<div style='width:100%; height:100%; overflow:auto; white-space:nowrap;'>
    <img src='" & varIMG & "' 
         style='width:" & zoomValue & "px; height:auto; display:inline-block;' />
</div>"

Y en la propiedad OnStart de nuestra aplicación ponemos esto:

Set(zoomValue; 3000)

En el botón rojo:

Set(varIMG;Blank())

En el botón del menos:

Set(zoomValue; Max(zoomValue * 0,8; 200))

En el botón del Reset:

Set(zoomValue; 3800)

y en el botón del más:

Set(zoomValue; Min(zoomValue * 1,2; 9000))

Y, como podéis comprobar he añadido una galería con la info de la lista de SP, para que sea de ayuda y veamos qué es lo que estamos generando.

Espero que os haya gustado. Desde luego yo lo he pasado en grande. Ahhh, el coste de utilizar Azure function es muy bajo con cada generación de organigrama.

Comparte este post

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies