Hola a todos,
Este post va a ser similar al post que había escrito hace un par de semanas sobre cómo construir una nube de palabras utilizando Python, Azure, VSC, Power Automate y Power Apps.
Os remito al post de referencia: https://excelsignum.com/2025/11/02/generar-nube-de-palabras-con-azure-functions-en-power-apps/
En este caso, lo que tenemos es una serie de palabras que hemos pedido a los empleados de una empresa, que indiquen 10 palabras que definan de alguna forma lo que sienten cuando se encuentran en nuestra empresa.

Estos datos que muestro aquí en Excel están en una lista de SharePoint, y lo que buscamos es generar algo diferente a una nube de palabras, por ejemplo, una espiral de burbujas, que muestre en el centro las que se repiten con mayor frecuencia y con burbujas más grandes y el resto que vaya decreciendo.
Para ello vamos a utilizar Azure Functions y después de generar nuestra función en VSC con Python: miFuncionDatosBurbuja
Os dejo aquí la función que tenéis que pegar en vuestro archivo init.py
import logging
import azure.functions as func
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import io
import base64
# -------------------------------
# STOPWORDS — AÑADE LAS QUE QUIERAS
# -------------------------------
STOPWORDS_ES = set([
"un", "una", "unas", "unos", "uno", "sobre", "todo", "también", "tras",
"otro", "algún", "alguno", "alguna", "algunos", "algunas", "ser", "es",
"soy", "eres", "somos", "sois", "estoy", "esta", "estamos", "estais",
"estan", "como", "en", "para", "porque", "estado", "estaba", "ante",
"antes", "siendo", "ambos", "pero", "por", "poder", "puede", "podria",
"pueden", "puedo", "mientras", "con", "muy", "más", "ya", "del", "los",
"las", "lo", "de", "la", "el", "y", "o", "u", "que", "qué", "a", "al"
])
# --------------------------------
# FUNCIÓN PRINCIPAL DE AZURE
# --------------------------------
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info("Generando gráfico de burbujas en espiral")
try:
data = req.get_json()
texto = data.get("texto", "")
# --------------------------------
# LIMPIEZA DE TEXTO
# --------------------------------
palabras = []
for palabra in texto.replace(",", " ").split():
palabra = palabra.strip().lower()
if palabra and palabra not in STOPWORDS_ES:
palabras.append(palabra)
# Conteo
from collections import Counter
conteo = Counter(palabras)
labels = list(conteo.keys())
counts = list(conteo.values())
N = len(labels)
# --------------------------------
# ESPIRAL COMPACTA TIPO FIBONACCI
# --------------------------------
a = 0.25 # separación compacta
b = 0.35 # crecimiento suave
theta = np.linspace(0, 4 * np.pi, N)
r = a + b * theta
x = r * np.cos(theta)
y = r * np.sin(theta)
sizes = [600 + c * 650 for c in counts]
# --------------------------------
# GRÁFICO
# --------------------------------
fig, ax = plt.subplots(figsize=(14, 10))
colors = plt.cm.Pastel1(np.linspace(0, 1, N))
ax.scatter(x, y, s=sizes, c=colors, alpha=0.45)
# -------------------------------
# TEXTO CENTRADO SOBRE BURBUJAS
# -------------------------------
for i, palabra in enumerate(labels):
ax.text(
x[i], y[i], palabra,
fontsize=14,
ha="center", va="center",
fontweight="bold"
)
# --------------------------------
# QUITAR EJES Y CENTRAR VISUAL
# --------------------------------
ax.axis("off")
ax.set_xlim(min(x)-2, max(x)+2)
ax.set_ylim(min(y)-2, max(y)+2)
# --------------------------------
# EXPORTAR A BASE64
# --------------------------------
buf = io.BytesIO()
plt.savefig(buf, format="png", bbox_inches="tight", dpi=150)
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
return func.HttpResponse(
body=f'{{"imagen": "{img_base64}"}}',
mimetype="application/json"
)
except Exception as e:
logging.error(f"Error: {str(e)}")
return func.HttpResponse(
body=f"Error procesando datos: {str(e)}",
status_code=500
)
Luego la desplegáis para poder obtener la URI y generar nuestro Automate.

En seleccionar lo obtendréis con el contenido dinámico, pero en Redactar debéis utilizar una expresión:
join(split(join(body('Seleccionar'), ','), ','), ' ')
Y seguimos:

El Cuerpo del HTTP que se pega directamente (no expresión):
{
"texto": "@{outputs('Redactar')}"
}
Y la respuesta como expresión
body('HTTP')?['imagen']
Y con esto, si pulsamos veremos esto en el control de imagen de nuestra App:

