Cómo hacer un gráfico de líneas con CSS
Los gráficos de líneas, barras y circulares son la base de los paneles y son los componentes básicos de cualquier conjunto de herramientas de visualización de datos. Claro, puedes usar SVG o una biblioteca de gráficos JavaScript como Chart.js o una herramienta compleja como D3 para crear esos gráficos, pero ¿qué pasa si no quieres cargar otra biblioteca en tu sitio web que ya tiene problemas de rendimiento?
Hay muchos artículos disponibles para crear gráficos de barras, gráficos de columnas y gráficos circulares solo con CSS, pero si solo deseas un gráfico de líneas básico, no tienes suerte. Si bien CSS puede “dibujar líneas” con bordes y similares, no existe un método claro para dibujar una línea de un punto a otro en un plano de coordenadas X e Y.
Bueno, ¡hay una manera! Si todo lo que necesita es un gráfico de líneas simples, no es necesario cargarlo en una enorme biblioteca de JavaScript ni siquiera utilizar SVG. Puedes crear todo lo que necesitas con solo CSS y un par de propiedades personalizadas en tu HTML. Sin embargo, una palabra de advertencia. Implica un poco de trigonometría. Si eso no te austó, entonces arremángate la camisa y ¡comencemos!
He aquí un vistazo de hacia dónde nos dirigimos:
Comenzamos con la línea de base.
Si está creando un gráfico de líneas a mano (como, literalmente, dibujando líneas en una hoja de papel cuadriculado), comenzaría creando los puntos y luego conectando esos puntos para formar las líneas. Si divides el proceso de esa manera, puedes recrear cualquier gráfico de líneas básicas en CSS.
Digamos que tenemos una matriz de datos para mostrar puntos en un sistema de coordenadas X e Y, donde los días de la semana caen a lo largo del eje X y los valores numéricos representan puntos en el eje Y.
[ { value: 25, dimension: "Monday" }, { value: 60, dimension: "Tuesday" }, { value: 45, dimension: "Wednesday" }, { value: 50, dimension: "Thursday" }, { value: 40, dimension: "Friday" }]
Creemos una lista desordenada para contener nuestros puntos de datos y aplicarle algunos estilos. Aquí está nuestro HTML:
figure ul li div data-value="25"/div /li li div data-value="60"/div /li li div data-value="45"/div /li li div data-value="50"/div /li li div data-value="40"/div /li /ul/figure
Un par de notas para recoger aquí. Primero, estamos envolviendo todo en un figure
elemento, que es una forma HTML semántica agradable de decir que se trata de contenido autónomo, lo que también nos brinda el beneficio opcional de usar un figcaption
, en caso de que lo necesitemos. En segundo lugar, observe que estamos almacenando los valores en un atributo de datos al que llamamos data-value
y que está contenido en su propio div dentro de un elemento de la lista desordenada. ¿Por qué utilizamos un div separado en lugar de poner la clase y el atributo en los elementos de la lista? Nos ayudará más adelante cuando lleguemos a dibujar líneas.
Por último, tenga en cuenta que tenemos una propiedad personalizada incorporada en el figure
elemento principal al que llamamos --widget-size
. Lo usaremos en el CSS, que veremos así:
/* The parent element */.css-chart { /* The chart borders */ border-bottom: 1px solid; border-left: 1px solid; /* The height, which is initially defined in the HTML */ height: var(--widget-size); /* A little breathing room should there be others items around the chart */ margin: 1em; /* Remove any padding so we have as much space to work with inside the element */ padding: 0; position: relative; /* The chart width, as defined in the HTML */ width: var(--widget-size);}
/* The unordered list holding the data points, no list styling and no spacing */.line-chart { list-style: none; margin: 0; padding: 0;}
/* Each point on the chart, each a 12px circle with a light border */.data-point { background-color: white; border: 2px solid lightblue; border-radius: 50%; height: 12px; position: absolute; width: 12px;}
El HTML y CSS anteriores nos darán este punto de partida no tan emocionante:
Representar puntos de datos
Eso no parece mucho todavía. Necesitamos una forma de dibujar cada punto de datos en sus respectivas coordenadas X e Y en nuestro futuro gráfico. En nuestro CSS, configuramos la .data-point
clase para que use posicionamiento absoluto y configuramos un ancho y alto fijos en su .css-chart
contenedor principal con una propiedad personalizada. Podemos usarlo para calcular nuestras posiciones X e Y.
Nuestra propiedad personalizada establece la altura del gráfico en 200 px y, en nuestra matriz de valores, el valor más grande es 60. Si configuramos ese punto de datos como el punto más alto en el eje Y del gráfico en 200 px, entonces podemos usar la proporción de cualquier valor en nuestros datos establecida en 60 y multiplíquelos por 200 para obtener la coordenada Y de todos nuestros puntos. Entonces nuestro valor más grande de 60 tendrá un valor Y que se puede calcular así:
(60 / 60) * 200 = 200px
Y nuestro valor más pequeño de 25 terminará con un valor Y calculado de la misma manera:
(25 / 60) * 200 = 83.33333333333334px
Obtener la coordinada Y para cada punto de datos es más fácil. Si espaciamos los puntos por igual en el gráfico, entonces podemos dividir el ancho del gráfico (200 px) por la cantidad de valores en nuestra matriz de datos (5) para obtener 40 px. Eso significa que el primer valor tendrá una coordenada X de 40px (para dejar un margen para un eje izquierdo si queremos uno), y el último valor tendrá una coordenada X de 200px.
¡Acabas de obtener matemáticas!
Por ahora, agregamos estilos en línea a cada uno de los divs en los elementos de la lista. Nuestro nuevo HTML se convierte en esto, donde los estilos en línea contienen el posicionamiento calculado para cada punto.
figure ul li div data-value="25"/div /li li div data-value="60"/div /li li div data-value="45"/div /li li div data-value="50"/div /li li div data-value="40"/div /li /ul/figure
¡Oye, eso se ve mucho mejor! Pero aunque puedes ver hacia dónde va esto, todavía no puedes llamarlo gráfico lineal. Ningún problema. Sólo necesitamos usar un poco más de matemáticas para terminar nuestro juego de conectar los puntos. Eche un vistazo a la imagen de nuestros puntos de datos renderizados nuevamente. ¿Puedes ver los triángulos que los conectan? Si no, tal vez la siguiente imagen te ayude:
¿Por qué es eso importante? Shhh, la respuesta viene a continuación.
Representante de segmentos de línea
¿Ves los triángulos ahora? Y no son simples triángulos cualquiera. Son el mejor tipo de triángulos (al menos para nuestros propósitos) ¡porque son triángulos rectángulos ! Cuando calculamos las coordenadas Y de nuestros puntos de datos anteriores, también estábamos calculando la longitud de un cateto de nuestro triángulo rectángulo (es decir, la “carrera” si lo considera como un escalón). Si calculamos la diferencia en la coordenada X de un punto al siguiente, eso nos dirá la longitud de otro lado de nuestro triángulo rectángulo (es decir, el “ascenso” de un escalón). Y con esos dos datos, podemos calcular la longitud de la hipotenusa mágica que, como resultado, es exactamente lo que necesitamos dibujar en la pantalla para conectar nuestros puntos y hacer un gráfico de líneas reales.
Por ejemplo, tomemos el segundo y tercer punto del gráfico.
!-- ... --
li div data-value="60"/div/lili div data-value="45"/div/li
!-- ... --
El segundo punto de datos tiene un valor Y de 200 y el tercer punto de datos tiene un valor Y de 150, por lo que el lado opuesto del triángulo que los conecta tiene una longitud de 200 menos 150, o 50. Tiene un lado adyacente que mide 40 píxeles de largo (la cantidad de espacio que ponemos entre cada uno de nuestros puntos).
Eso significa que la longitud de la hipotenusa es la raíz cuadrada de 50 al cuadrado más 40 al cuadrado, o 64,03124237432849.
Creemos otro div dentro de cada elemento de la lista en el gráfico que servirá como hipotenusa de un triángulo dibujado desde ese punto. Luego estableceremos una propiedad personalizada en línea en nuestro nuevo div que contiene la longitud de esa hipotenusa.
!-- ... --
li div data-value="60"/div div/div/li
!-- ... --
Mientras estamos en esto, nuestros segmentos de línea necesitarán conocer sus coordenadas X e Y adecuadas, así que eliminemos los estilos en línea de nuestros .data-point
elementos y agreguemos propiedades personalizadas de CSS a su elemento principal (el li
elemento). Llamemos a estas propiedades, creativamente, --x
y --y
. Nuestros puntos de datos no necesitan conocer la hipotenusa (la longitud de nuestro segmento de línea), por lo que podemos agregar una propiedad personalizada de CSS para la longitud de la hipotenusa directamente a nuestro archivo .line-segment
. Entonces ahora nuestro HTML se verá así:
!-- ... --
li div data-value="60"/div div/div/li
!-- ... --
Necesitaremos actualizar nuestro CSS para posicionar los puntos de datos con esas nuevas propiedades personalizadas y darle estilo al nuevo .line-segment
div que agregamos al marcado:
.data-point { /* Same as before */
bottom: var(--y); left: var(--x);}
.line-segment { background-color: blue; bottom: var(--y); height: 3px; left: var(--x); position: absolute; width: calc(var(--hypotenuse) * 1px);}
Bueno, ahora tenemos segmentos de línea, pero esto no es en absoluto lo que queremos. Para obtener un gráfico de líneas funcional, necesitamos aplicar una transformación. Pero primero, arreglemos un par de cosas.
En primer lugar, nuestros segmentos de línea se alinean con la parte inferior de nuestros puntos de datos, pero queremos que el origen de los segmentos de línea sea el centro de los círculos de los puntos de datos. Podemos solucionarlo con un cambio rápido de CSS en nuestros .data-point
estilos. Necesitamos ajustar su posición X e Y para tener en cuenta tanto el tamaño del punto de datos y su borde como también el ancho del segmento de línea.
.data-point { /* ... */
/* The data points have a radius of 8px and the line segment has a width of 3px, so we split the difference to center the data points on the line segment origins */ bottom: calc(var(--y) - 6.5px); left: calc(var(--x) - 9.5px);}
En segundo lugar, nuestros segmentos de línea se representan encima de los puntos de datos en lugar de detrás de ellos. Podemos solucionar esto poniendo el segmento de línea primero en nuestro HTML:
!-- ... --
li div/div div data-value="60"/div/li
!-- ... --
Aplicando transformaciones, FTW
Ya casi lo tenemos. Sólo necesitamos hacer un último poco de cálculo. Específicamente, necesitamos encontrar la medida del ángulo que mira al lado opuesto de nuestro triángulo rectángulo y luego rotar nuestro segmento de línea esa misma cantidad de grados.
¿Como hacemos eso? ¡Trigonometría! Quizás recuerdes el pequeño truco mnemotécnico para recordar cómo se calculan el seno, el coseno y la tangente:
- SOH (Seno = Opuesto sobre Hipotenusa
- CAH (Coseno = Adyacente a la hipotenusa)
- TOA (Tangente = Opuesto sobre Adyacente)
Puedes usar cualquiera de ellos porque conocemos la longitud de los tres lados de nuestro triángulo rectángulo. Elegí seno, por lo que nos deja con esta ecuación:
sin(x) = Opposite / Hypotenuse
La respuesta a esa ecuación nos dirá cómo rotar cada segmento de línea para que se conecte con el siguiente punto de datos. Podemos hacer esto rápidamente en JavaScript usando Math.asin(Opposite / Hypotenuse)
. Sin embargo, nos dará la respuesta en radianes, por lo que tendremos que multiplicar el resultado por (180 / Math.PI)
.
Usando el ejemplo de nuestro segundo punto de datos anterior, ya descubrimos que el lado opuesto tiene una longitud de 50 y la hipotenusa tiene una longitud de 64.03124237432849, por lo que podemos reescribir nuestra ecuación de esta manera:
sin(x) = 50 / 64.03124237432849 = 51.34019174590991
¡Ese es el ángulo que estamos buscando! Necesitamos resolver esa ecuación para cada uno de nuestros puntos de datos y luego pasar el valor como una propiedad personalizada de CSS en nuestros .line-segment
elementos. Eso nos dará un HTML que se ve así:
!-- ... --
li div data-value="60"/div div/div/li
!-- ... --
Y aquí es donde podemos aplicar esas propiedades en CSS:
.line-segment { /* ... */ transform: rotate(calc(var(--angle) * 1deg)); width: calc(var(--hypotenuse) * 1px);}
Ahora, cuando renderizamos eso, ¡tenemos nuestros segmentos de línea!
¿Esperar lo? Nuestros segmentos de línea están por todas partes. ¿Ahora que? Correcto. Por defecto, transform: rotate()
gira alrededor del centro del elemento transformado. Queremos que la rotación se produzca desde la esquina inferior izquierda para alejarse de nuestro punto de datos actual hacia el siguiente. Eso significa que necesitamos establecer una propiedad CSS más en nuestra .line-segment
clase.
.line-segment { /* ... */ transform: rotate(calc(var(--angle) * 1deg)); transform-origin: left bottom; width: calc(var(--hypotenuse) * 1px);}
Y ahora, cuando lo renderizamos, finalmente obtenemos el gráfico de líneas solo CSS que estábamos esperando.
Nota importante: cuando calcule el valor del lado opuesto (el "aumento"), asegúrese de que se calcule como la "posición Y del punto de datos actual" menos la "posición Y del siguiente punto de datos". Eso dará como resultado un valor negativo cuando el siguiente punto de datos sea un valor mayor (más arriba en el gráfico) que el punto de datos actual, lo que dará como resultado una rotación negativa. Así nos aseguramos de que la línea tenga una pendiente hacia arriba.
Cuándo utilizar este tipo de gráfico
Este enfoque es excelente para un sitio estático simple o para un sitio dinámico que utiliza contenido generado por el servidor. Por supuesto, también se puede utilizar en un sitio con contenido generado dinámicamente del lado del cliente, pero luego volverá a ejecutar JavaScript en el cliente. El CodePen en la parte superior de esta publicación muestra un ejemplo de generación dinámica del lado del cliente de este gráfico de líneas.
La función CSS calc()
es muy útil, pero no puede calcular el seno, el coseno y la tangente por nosotros. Eso significa que tendría que calcular sus valores a mano o escribir una función rápida (del lado del cliente o del servidor) para generar los valores necesarios (X, Y, hipotenusa y ángulo) para nuestras propiedades personalizadas de CSS.
Sé que algunos de ustedes superaron esto y sentirán que no es CSS básico si requiere un script para calcular los valores, y eso es justo. El punto es que toda la representación del gráfico se realiza en CSS. Los puntos de datos y las líneas que los conectan están hechos con elementos HTML y CSS que funcionan a la perfección, incluso en un entorno renderizado estáticamente sin JavaScript habilitado. Y quizás lo más importante es que no es necesario descargar otra biblioteca inflada solo para representar un gráfico lineal simple en su página.
Mejoras potenciales
Como ocurre con todo, siempre hay algo que podemos hacer para llevar las cosas al siguiente nivel. En este caso, creo que hay tres áreas en las que este enfoque podría mejorarse.
Sensibilidad
El enfoque que he descrito utiliza un tamaño fijo para las dimensiones del gráfico, que es exactamente lo que no queremos en un diseño responsivo. Podemos solucionar esta limitación si podemos ejecutar JavaScript en el cliente. En lugar de codificar el tamaño de nuestro gráfico, podemos establecer una propiedad personalizada de CSS (¿recuerda nuestra --widget-size
propiedad?), basar todos los cálculos en ella y actualizar esa propiedad cuando el contenedor o la ventana se muestre inicialmente o cambie de tamaño usando alguna forma de consulta de contenedor o un detector de cambio de tamaño de ventana.
Información sobre herramientas
Podríamos agregar un ::before
pseudoelemento a .data-point para mostrar la data-value
información que contiene en una información sobre herramientas al pasar el cursor sobre el punto de datos. Este es un toque agradable que ayuda a convertir nuestro sencillo gráfico en un producto terminado.
Líneas de eje
¿Observa que los ejes del gráfico no están etiquetados? Podríamos distribuir etiquetas que representen el valor más alto, cero y cualquier número de puntos entre ellos en el eje.
Márgenes
Intenté mantener los números lo más simples posible para este artículo, pero en el mundo real, probablemente querrás incluir algunos márgenes en el gráfico para que los puntos de datos no se superpongan a los bordes extremos de su contenedor. Eso podría ser tan simple como restar el ancho de un punto de datos del rango de sus coordenadas y. Para las coordenadas X, también puedes eliminar el ancho de un punto de datos del ancho total del gráfico antes de dividirlo en regiones iguales.
¡Y ahí lo tienes! Simplemente analizamos detenidamente un enfoque para crear gráficos en CSS y ni siquiera necesitábamos una biblioteca o alguna otra dependencia de terceros para que funcionara.
Deja un comentario