nube.js, generando word clouds utilizando canvas

nube.js

nube.js es una pequeña librería que tiene como objetivo generar nubes de palabras utilizando tecnologías nativas del browser, especialmente del <canvas>.

Motivación

Luego de ver la propuesta de wordle decidí tomar como ‘proyecto de fin de semana’ desarrollar una herramienta que permita generar nube de palabras directamente en el browser, utilizando tecnologías de moda, como es el caso del <canvas>.

Ejemplos

Algunos ejemplos de uso:

  • Uso básico
    Generando un word cloud en base a un texto dado.
  • Exportando como imagen
    Exportando el resultado de un input a imagen (en base64)
  • jquery.nube.js (trabajo en progreso)
    Trabajando sobre elementos DOM específicos, en este caso sobre el tag cloud de este blog.

Herramientas

Siendo un proyecto ‘de fin de semana’ no tiene demasiado sentido construir desde cero. Para nube.js utilice dos herramientas con las que vengo trabajando hace un tiempo, fabric.js y Cufon.

fabric.js

Este gran proyecto de @kangax permite manipular e interactuar con objetos canvas de manera compleja sin que ello signifique algún tipo de dificultad. Provee de toda la interacción necesaria para este caso y más aún, permitendo que el usuario posiciones, rote y altere el tamaño de los objetos de manera intuitiva.

Cufon

Esta librería (al igual que otras tantas) permite generar textos con tipografías no estándar sin utilizar Flash. La elección en este caso está dada por el hecho de que fabric.js la utiliza para renderizar textos además de que las tipografías disponibles son prácticamente infinitas (cufonfonts.com acusa más de 9.000 tipografías!).

Problemas

Si bien el desarrollo fue relativamente sencillo, resumo algunas de las problematicas que surgieron y sus correspondientes ‘soluciones’.

Posicionar aleatoriamente evitando colisiones

Obviamente, la idea básica de la librería es generar palabras y posicionarlas sin que colisionen.

El proceso actual consiste en renderizar el objeto en una posición aleatoria y luego en base a su tamaño y posición compararlo con cada una de las palabras ya renderizadas para verificar si colisionan o no. Esta comparación es bastante sencilla una vez que se entiende el comportamiento de fabric.js.

Las distancias de un objeto con respecto al punto (top: 0, left: 0) están evaluadas desde el centro del objeto y no desde la punta superior izquierda como se puede suponer. Además está el hecho de que al ser distancias relativas al centro hay que tener una ingeniería adicional para casos de rotación distintos.

En la siguiente imagen se puede observar un caso donde las distancias al eje superior son iguales, pero en el segundo caso se puede apreciar un impacto de los objetos:

Rotacion y fabric.js

Por otro lado, hay que considerar que el texto debería agruparse en una posición (inicialmente el centro) y luego tratar de ubicarse lo más próximo al mismo, teniendo en cuenta además que el crecimiento de la nube debería respetar cierto formato (apaisado para canvas apaisados por ejemplo).
Este comportamiento es bastante claro si miramos el código fuente:

Nube.prototype.setWordPosition = function (fabricText) {
    var top, left, centerY, centerX, seedY, seedX;

    seedY = seedX = 0;
    centerY = this.height / 2;
    centerX = this.width / 2;

	// Mientras colisiones, el texto es reubicado
    while (this.collides(fabricText)) {
        left = Nube.random(centerX - seedX, centerX + seedX);
        top = Nube.random(centerY - seedY, centerY + seedY);

        fabricText.set({
            left: left,
            top: top
        });

		// Cada iteracion hace crecer el area
		// de posibles posiciones
        seedY += this.stepY;
        seedX += this.stepX;
    }

    return fabricText;
};

Que básicamente aumenta progresivamente el area candidata para ubicar la palabra. Dentro del area candidata, la posicion se define aleatoriamente (lógica que puede llevar palabras a malos lugares).

Aumento iterativo de area candidata

Este proceso de posicionamiento de palabras (puede y) debería optimizarse ya que es bastante costoso, lo cual nos lleva al siguiente problema.

Gestionar las operaciones costosas sin bloquear el browser

Como correctamente se puede suponer, posicionar las palabras dentro del canvas es un proceso costoso, y como tal requiere una cantidad de tiempo que el browser que implicaría bloquearlo (y en algunos casos colgarlo).

Para evitar cualquier tipo de problema, hay que dividir el trabajo en pequeñas partes e ir ejecutandolas de forma diferida. Este tipo de soluciones está mejor explicados en la entrada ‘Timed array processing in javascript‘ de Nicholas Zakas (ex Yahoo).

De esta manera, la generación de la nube se vuelve un proceso asincronico. El callback onComplete permite saber cuando finalizó el proceso:

var n = new Nube({
    text: $('textarea').val(),
    onComplete: function () {
		alert('Complete!');
    },
    fonts: ['DejaVu_Serif_400']
});

n.renderTo($('.placeholder').get(0));

Densidad de la nube

Uno de los grandes problemas que genera trabajar con las textos como objetos fabric, es que las dimensiones del mismo estan dadas en la forma de caja, atentando de forma directa contra la densidad de texto ya que la separación mínima de las palabras se corresponde con las dimensiones del objeto, tal como se puede apreciar en la siguiente imagen:

Baja densidad de texto

Para solventar esta problematica se puede manipular la verificación de colisión de texto agregado suponiendo que su area es menor a la real, tal como funcionaría tener margenes negativos en un elemento del HTML.

El margen en cuestión puede ser fijo o proporcional al tamaño de la palabra, lo cual es más conveniente para este tipo de situaciones.

// Define el margen de cada palabra
setWordMargin: function (word, fontSize, fabricObject) {
    // Margenes negativos aumentan la densidad
    return {
        top: -fontSize * 0.1, // Margen proporcional al tamaño
        right: -2, // Margen estatico
        bottom: -fontSize * 0.2,
        left: -fontSize * 0.2
    };
}

Posición de texto ‘incorrecta’

Otro problema relacionado a las tipografías, es el hecho de que algunas de ellas no están correctamente contenidas dentro del objeto en sí, sino que sobresalen fuera del mismo. Esto genera la problematica de que las distintas palabras pueden solaparse, perjudicando así la lectura.

Posición de texto incorrecta

Como workaround para este problema se puede utilizar un margen positivo sobre el lado sobresaliente (por lo general el izquierdo). Obviamente, este margen debería ser proporcional al tamaño de la palabra, recordemos ademas que esta situación varía con las distintas fonts.

setWordMargin: function (word, fontSize, fabricObject) {
    return {
        top: 0,
        right: 0,
        bottom: 0,
        // Expandimos el lado izquierdo
        left: fontSize * 0.2
    };
}

Próximos pasos

La librería en su estado actual (versión 0.1) es una prueba de concepto de la idea, que puede completarse y mejorarse en muchos aspectos (sobre todo en cuanto a calidad del código).

El TODO actual se puede resumir a:

  • Generar documentación, aunque sea precaria 🙂
  • Buildear a un único archivo (incluyendo fabric.js y alguna font por defecto).
  • Buildear una versión customizada de fabric.js (sin las clases no utilizadas).
  • Agregar test funcionales.
  • Completar jquery.nube.js.
  • Optimizar el posicionamiento de palabras para reducir el tiempo requerido.
  • Agregar soporte para incluir imágenes dentro de la nube.
  • Validar que las palabras se posicionen dentro del area visible.

Leer más:



2 views shared on this article. Join in...

  1. Raul dice:

    Puede ser interesante que a la hora de comprobar colisiones tengas en cuenta las palabras existentes como si fuesen letras individuales y a la hora de insertar una palabra nueva la trataras como un bloque. O sea, lo que ya existe son letras, y lo que quiero intentar insertar es un bloque que contiene la pablara, pero luego inserto las letras una al lado de otra o encima de otra según la orientación.

    • Todo lo referido a la validación de colisiones necesita una vuelta de tuerca ya que no es demasiado performante. Cuando pueda voy a ver si puedo refactorear dicha parte.

      Gracias por las ideas, las tendré en cuenta 🙂

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>