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.
