Una representación visual convierte pares ordenados, matrices y grafos en patrones que podemos explorar. En este tema construiremos visualizaciones con SVG y Canvas para comprender propiedades de relaciones y funciones, detectar errores y comunicar resultados.
Una relación discreta es un conjunto de pares ordenados. Por ejemplo, R = {(1, 2), (1, 3), (2, 3)} puede escribirse como texto, tabla, matriz, diagrama de flechas o grafo. Todas las representaciones describen el mismo objeto, pero cada una hace visibles propiedades diferentes.
La visualización computacional es el proceso de transformar datos en elementos gráficos mediante reglas reproducibles. No consiste en decorar una pantalla: una buena visualización conserva la información matemática y ayuda a responder una pregunta.
La visualización resulta especialmente útil cuando el conjunto es finito, cuando queremos comparar alternativas o cuando necesitamos observar una evolución paso a paso. Si el volumen de datos es grande, el dibujo debe resumir y permitir filtrar: mostrar todo no siempre significa explicar mejor.
Consideremos A = {1, 2, 3, 4}, B = {a, b, c} y la relación R = {(1, a), (1, b), (2, b), (3, c), (4, c)}. El par (2, b) se dibuja como una flecha desde 2 hasta b.
El diagrama permite observar inmediatamente que 1 tiene dos imágenes. Por lo tanto, R es una relación, pero no es una función de A en B. En cambio, cada elemento de B recibe al menos una flecha, por lo que sí es sobreyectiva como relación de asignación si se ignora la condición funcional.
En un programa conviene separar los datos de su representación. La relación puede guardarse como arreglo de pares y una función de dibujo puede recibir ese arreglo. Así se puede cambiar el estilo, el tamaño o el formato sin modificar la definición matemática.
const A = [1, 2, 3, 4];
const B = ['a', 'b', 'c'];
const R = [[1, 'a'], [1, 'b'], [2, 'b'], [3, 'c'], [4, 'c']];
function imagenDe(x, relacion) {
return relacion.filter(([entrada]) => entrada === x)
.map(([, salida]) => salida);
}
// La cantidad de salidas de cada entrada permite detectar una función.
const esFuncion = A.every(x => imagenDe(x, R).length === 1);
console.log('Conjunto A:', A);
console.log('Imagen de 1:', imagenDe(1, R));
console.log('¿La relación es función de A en B?:', esFuncion);Para visualizar una función f: A → B numérica, ubicamos cada entrada x en el eje horizontal y su salida f(x) en el eje vertical. Solo se dibujan los valores que pertenecen al dominio; por eso una función discreta aparece como un conjunto de puntos, no necesariamente como una línea continua.
Unir puntos con segmentos puede sugerir que existen valores intermedios. Esa interpolación es válida solo si el modelo lo justifica. Para una función definida sobre números naturales, los puntos separados comunican mejor que una curva que inventa valores.
El escalado transforma coordenadas matemáticas en píxeles. Si el área de dibujo mide ancho × alto, una transformación típica es:
const margen = 40, xmin = 0, ymin = 0;
const escalaX = 20, escalaY = 15, alto = 300;
const x = 4, y = 6;
const px = margen + (x - xmin) * escalaX;
const py = alto - margen - (y - ymin) * escalaY;
// El eje vertical de Canvas crece hacia abajo; por eso se resta py.
console.log('Coordenada matemática:', { x, y });
console.log('Coordenada de pantalla:', { px, py });La escala debe ser coherente en ambos ejes cuando se quiere preservar pendientes y distancias. También es importante mostrar el origen, las unidades y una leyenda que indique el dominio utilizado.
Las propiedades de una función pueden expresarse visualmente:
Un algoritmo puede verificar esas propiedades antes de dibujar. Así la visualización no depende de una inspección subjetiva:
function analizarFuncion(dominio, codominio, pares) {
const salidas = new Map();
for (const [x, y] of pares) {
if (!dominio.includes(x) || !codominio.includes(y)) return { valida: false };
if (salidas.has(x)) return { valida: false, motivo: 'entrada repetida' };
salidas.set(x, y);
}
const valores = [...salidas.values()];
return {
valida: dominio.every(x => salidas.has(x)),
inyectiva: new Set(valores).size === valores.length,
sobreyectiva: codominio.every(y => valores.includes(y))
};
}
const analisis = analizarFuncion(
[1, 2, 3],
['a', 'b'],
[[1, 'a'], [2, 'b'], [3, 'a']]
);
console.log('Análisis de la función:', analisis);Una relación sobre un conjunto finito puede representarse mediante una matriz booleana. Para A = {1, 2, 3, 4}, la celda (i, j) vale 1 si i R j y 0 en caso contrario. Un mapa de calor reemplaza 1 por un color intenso y 0 por un color tenue.
Este formato es útil para observar simetría, densidad y patrones. Una matriz simétrica respecto de la diagonal representa una relación simétrica; una diagonal completa indica reflexividad; la cantidad de unos por fila muestra cuántas imágenes tiene cada elemento.
Para no confundir un valor ausente con un dato desconocido, la interfaz debe distinguir al menos tres estados cuando corresponda: verdadero, falso y sin información. La paleta también debe funcionar para personas con daltonismo; las etiquetas y los valores numéricos siguen siendo necesarios.
Cuando los elementos pertenecen al mismo conjunto, una relación binaria se dibuja como un grafo dirigido. Cada elemento es un vértice y cada par (u, v) es una arista orientada de u a v. Los lazos representan pares como (u, u).
En una red de dependencias, por ejemplo, una flecha A → B puede significar “A necesita a B”. La visualización permite detectar ciclos, vértices aislados y caminos. Es importante no confundir la dirección de la arista con una relación simétrica: que A se conecte con B no implica que B se conecte con A.
Para grafos pequeños es suficiente usar SVG: cada vértice puede ser un círculo y cada arista una línea con marcador de flecha. Para miles de elementos se suelen usar técnicas de agrupamiento, filtros, lienzo (Canvas) o WebGL, porque dibujar cada etiqueta puede volver lenta la interfaz.
const aristas = [['A', 'B'], ['A', 'C'], ['B', 'D'], ['C', 'D'], ['D', 'E'], ['E', 'F']];
function vecinos(origen, relaciones) {
return relaciones.filter(([u]) => u === origen).map(([, v]) => v);
}
// El mismo arreglo alimenta una vista SVG, una tabla de adyacencia
// o una búsqueda en anchura. Datos y presentación permanecen separados.
console.log('Vecinos de A:', vecinos('A', aristas));
console.log('Vecinos de C:', vecinos('C', aristas));Una animación agrega una dimensión temporal. Puede mostrar cómo se construye una clausura transitiva, cómo se propaga una búsqueda o cómo cambia una función al variar un parámetro. Cada paso debe corresponder a un estado matemático bien definido; mover objetos sin explicar qué representa el movimiento solo añade ruido.
Una estrategia sencilla es mantener un índice paso y dibujar únicamente los pares cuyo índice sea menor o igual. Para reproducibilidad, se puede pausar, avanzar y volver al estado inicial. Esto facilita que el estudiante compruebe qué arista se incorporó y por qué.
let paso = 0;
const estados = [
[['A', 'B']],
[['A', 'B'], ['B', 'C']],
[['A', 'B'], ['B', 'C'], ['A', 'C']] // aparece por transitividad
];
function avanzar() {
paso = Math.min(paso + 1, estados.length - 1);
console.log(`Paso ${paso}:`, estados[paso]);
}
avanzar();
avanzar();En visualizaciones educativas, la animación debe poder detenerse y no debe ser la única forma de acceder a la información. Una tabla equivalente, texto alternativo o una descripción del estado hacen que el contenido siga siendo comprensible.
| Decisión | Buena práctica | Riesgo frecuente |
|---|---|---|
| Escala | Indicar unidades, límites y origen. | Exagerar una diferencia cambiando el eje. |
| Color | Usar una paleta consistente y con contraste. | Usar color como única codificación. |
| Etiquetas | Nombrar conjuntos, ejes y relaciones. | Obligar a adivinar qué significa cada nodo. |
| Volumen | Filtrar, agrupar y permitir explorar. | Superponer miles de elementos ilegibles. |
| Interacción | Mostrar estado, selección y acción disponible. | Animar sin explicar qué cambió. |
Una visualización puede ser formalmente correcta y aun así inducir a errores. Por ejemplo, una línea que une puntos de una función discreta puede sugerir continuidad; una flecha sin punta puede ocultar la dirección; y un grafo sin leyenda puede hacer pasar una arista por una simple conexión no orientada.
Un diseño mantenible separa cuatro responsabilidades:
const modelo = {
dominio: [1, 2, 3],
codominio: ['a', 'b'],
pares: [[1, 'a'], [2, 'b'], [3, 'a']]
};
function analizar(modelo) {
const imagen = new Set(modelo.pares.map(([, y]) => y));
return { imagen: [...imagen], sobreyectiva: imagen.size === modelo.codominio.length };
}
function renderizar(modelo, resultado) {
// La vista consume datos ya validados y dibuja sin cambiar el modelo.
console.log(`Imagen: ${resultado.imagen.join(', ')}`);
console.log(`¿Es sobreyectiva?: ${resultado.sobreyectiva}`);
}
const resultado = analizar(modelo);
renderizar(modelo, resultado);Esta separación permite probar el análisis sin abrir un navegador y cambiar SVG por Canvas sin reescribir las reglas matemáticas. También facilita guardar un estado, compartir una configuración o exportar una tabla.
Visualizar relaciones y funciones es traducir una estructura matemática a una representación que podamos recorrer con la vista y con la interacción. El objetivo no es reemplazar la definición formal, sino hacer explícitas sus consecuencias: qué elementos se relacionan, cuántas imágenes tiene una entrada, si existe un camino, dónde aparece un ciclo o qué patrón tiene una matriz.
Cuando el modelo está separado de la vista, JavaScript permite cambiar entre diagramas, gráficas, matrices y redes sin perder rigor. La mejor visualización es la que permite formular una pregunta, encontrar la evidencia y volver a los datos originales para comprobarla.
Una relación bien visualizada no solo se ve: se puede explorar, verificar y explicar.