Un simple Mapa de Calor en Canvas
Cómo realizar un mapa de calor personalizado sobre GoogleMaps
Índice de contenidos
- Cómo usar D3 con WebComponents.
- Gráfico de Red con D3.js en Canvas.
- Un simple Mapa de Calor en Canvas.
Introducción — ¿Qué es un mapa de calor?
A grandes rasgos seguro que todos nos hacemos una idea de lo que es un mapa de calor, pero creo que es interesante recorrer brevemente cómo se construye esta visualización en la mayoría de casos y por qué la solución que implementaremos en este artículo te puede resultar interesante.
Cuando buscamos sobre mapas de calor, nos encontramos habitualmente con diagramas de cuadrículas con un gradientes de colores en función del número de items dentro de cada cuadrícula. Esto es, dividimos el espacio de trabajo en pequeñas porciones, realizando un mallado normalmente rectangular.
Así, cada dato será asignado a una única celda del mallado. Sin embargo, en muchas ocasiones nuestros puntos tienen un radio de influencia o acción y tendremos que evaluar un punto y todos los valores dentro del radio para inputarlos en aquellas celdas que ocupen. Esto implica hacer una discretización de una variable continua en un proceso denominado rasterización.
Por otro lado, la densidad de celdas puede provocar un efecto “pixelado” y no una variación gradual de los colores. Para evitar esto, podemos aumentar el número de celdas en las que se divide el diagrama, pero implicará un aumento de las operaciones necesarias para generar el mapa. Una operación que escala de forma cuadrática respecto al número de celdas.
Además, hay situaciones en las que la representación se realiza sobre una superficie amplia, como las geográficas, que requiere un mallado fino para evitar un estética pixelada, lo que provoca una caída drástica del rendimiento o un renderizado lento.
…y qué otra opción hay?
Otra opción muy simple es la que vamos a ver en este post. La estrategia es completamente diferente; no realizaremos un mallado en el que ubicar los puntos o el radio de acción de nuestros datos; en vez de eso, dibujaremos un círculo degradado en escala de grises en el que el centro será negro y el extremo será transparente, por cada elemento de nuestro dataset.
Así, al dibujar los círculos solapados unos con otros, se empezará a acumular un nivel de gris cada vez más oscuro hasta llegar al color completamente negro donde haya más puntos cerca. Al terminar de dibujar todos los nodos, tendremos un mapa en tonalidades de gris que podremos colorear en función de los valores de los píxeles y conseguir así una escala de colores que represente la distribución de nuestros datos. Obteniendo un resultado como el de la imagen de cabecera de este artículo.
Representación de un dato
La estructura de datos tendrá los siguientes elementos como mínimo:
Cada dato será representado como un círculo con centro en latitude
, longitude
y radio radius
. Además, el círculo será un gradiente radial en tonalidades de gris, siendo el centro un punto negro y al llegar al valor exterior del radio será transparente.
Para ello utilizaremos un Canvas a modo de pincel.
Debido a que la propiedad que utilizaremos de Canvas es shadowBlur
, esta se sitúa en el origen del círculo y tenemos que desfasar la sombra para poder mostrar solo el gradiente.
Para conseguir esto, situamos el círculo fuera del lienzo y desfasamos la sombra para que coincida en el centro. Tal y como podemos ver en la imagen.
Esta operación se realiza tal y como podemos observar en el siguiente fragmento de código.
Además, en la función anterior, realizamos la instanciación de todos los tipos de círculos que tienen nuestros nodos, ya que cada elemento del dataset puede tener un radio de acción diferente. Es importante destacar que el radio es facilitado en kilómetros, por lo que se utiliza el parámetro normalizer
para transformar la unidad a píxeles en función del tamaño del elemento Canvas.
Una vez tenemos definidos los pinceles, recorremos todos los datos y vamos añadiendo círculos por cada uno de ellos. Al final, tendremos un mapa de tonalidades de gris como el que se aprecia en la imagen.
Definiendo los colores
El color del mapa lo cambiaremos utilizando un degradado de colores que crearemos a medida. En concreto, cada color lo extraeremos del grado de transparencia o canal alpha
de cada píxel del lienzo del mapa. Así, las zonas más traslúcidas son aquellas donde menos coincidencia de puntos hay y las zonas mas opacas es donde más volumen de incidencia existe.
Para definir el gradiente de colores utilizaremos un Canvas auxiliar. Crearemos un rectángulo con grosor de un pixel y altura 256 e introduciremos distintos colores tal y como se puede ver en la imagen. Además, en el siguiente fragmento de código podemos ver cómo realizar esta operación.
Coloreando el mapa
Una vez tenemos el lienzo y la paleta definidos, toca recolorear el Canvas del mapa, para lo que recorreremos todos los pixeles y cambiaremos su color. Esta operación es costosa pero inferior al del resto de alternativas, ya que no es necesario recorrer multiples veces los datos como sería el caso si realizáramos un mallado.
La operación de coloreado es tan sencilla como lo que se puede ver en el siguiente fragmento de código:
Es importante notar que en el array de píxeles cada uno ocupa 4 posiciones; un valor de 0 a 255 para el rojo, otro para el verde, otro para el azul y otro para la transparencia (RGBa).
Como podemos ver, utilizamos el valor de opacidad para acceder a la variable gradient
que es el canvas auxiliar donde hemos generado los colores.
Conclusión
Utilizando esta estrategia conseguimos un resultado mucho más suave y menos costoso, sobre todo cuando el volumen de puntos es elevado.
El elemento Canvas en el que se dibuja el mapa de calor es colocado sobre Google Maps a través de una propiedad que nos ofrece denominada Overlay
. No he añadido cómo realizar estas operaciones por no extenderme, pero si estás interesado en ver cómo se hace al detalle aquí está el repositorio con el código completo; solo necesitarás tu propio API Key de Google Maps que puedes obtener desde aquí.
En este caso es importante notar que, al utilizar un mapa de píxeles, tenemos una saturación en los valores que podemos representar; en concreto valores que oscilan entre 0 y 255, ya que son los valores de transparencia disponibles y por lo tanto, saturamos el valor máximo con facilidad. Para evitar esto, solo sería necesario utilizar una matriz de píxeles para guardar los valores de coincidencia y aplicar una escala en función de la gama de valores que queramos representar.