
Construyendo un gráfico de anillos con Vue y SVG

ÍndiceMmm… donut prohibido”.
– Homero Simpson
Recientemente necesité hacer un gráfico de anillos para un panel de informes en el trabajo. La maqueta que obtuve se parecía a esto:
Mi gráfico tenía algunos requisitos básicos. Necesitaba:
- Calcule dinámicamente sus segmentos segmentados en un conjunto arbitrario de valores.
- tener etiquetas
- Escala bien en todos los tamaños de pantalla y dispositivos.
- Ser compatible con varios navegadores hasta Internet Explorer 11
- ser accesible
- Ser reutilizable en la interfaz Vue.js de mi trabajo.
También quería algo que pudiera animar más tarde si fuera necesario. Todo esto parecía un trabajo para SVG.
Se puede acceder a los SVG desde el primer momento (el W3C tiene una sección completa sobre esto) y se pueden hacer más accesibles mediante entradas adicionales. Y, como funcionan con datos, son un candidato perfecto para la visualización dinámica.
Hay muchos artículos sobre el tema, incluidos dos de Chris ( aquí y aquí ) y uno súper reciente de Burke Holland. No utilicé D3 para este proyecto porque la aplicación no necesitaba la sobrecarga de esa biblioteca.
Creé el gráfico como un componente de Vue para mi proyecto, pero puedes hacerlo fácilmente con JavaScript básico, HTML y CSS.
Aquí está el producto terminado:
Reinventando elruedacirculo
Como cualquier desarrollador que se precie, lo primero que hice fue buscar en Google para ver si alguien más ya había hecho esto. Luego, como dijo el mismo desarrollador, descarté la solución prediseñada a favor de la mía propia.
El mayor éxito del “gráfico de anillos SVG” es este artículo, que describe cómo usar stroke-dasharray
y stroke-dashoffset
dibujar múltiples círculos superpuestos y crear la ilusión de un solo círculo segmentado (más sobre esto en breve).
Realmente me gusta el concepto de superposición, pero me resulta confuso volver a calcular ambos stroke-dasharray
valores stroke-dashoffset
. ¿Por qué no establecer un valor fijo stroke-dasharrary
y luego rotar cada círculo con un transform
? También era necesario agregar etiquetas a cada segmento, lo cual no se cubrió en el tutorial.
dibujando una linea
Antes de que podamos crear un gráfico de anillos dinámicos, primero debemos comprender cómo funciona el dibujo lineal SVG. Si no has leído el excelente dibujo lineal animado en SVG de Jake Archibald. Chris también tiene una buena visión general.
Estos artículos proporcionan la mayor parte del contexto que necesitará, pero brevemente, SVG tiene dos atributos de presentación: stroke-dasharray
y stroke-dashoffset
.
stroke-dasharray
define una serie de guiones y espacios que se utilizan para pintar el contorno de una forma. Puedes tomar cero, uno o dos valores. El primer valor define la longitud del guión; el segundo define la longitud del espacio.
stroke-dashoffset
, por otro lado, define dónde comienza el conjunto de guiones y espacios. Si los valores stroke-dasharray
y stroke-dashoffset
son la longitud de la línea y son iguales, toda la línea es visible porque le estamos diciendo al desplazamiento (donde comienza la matriz de guiones) que comienza al final de la línea. Si stroke-dasharray
es la longitud de la línea, pero es stroke-dashoffset
0, entonces la línea es invisible porque estamos compensando la parte renderizada del guión en toda su longitud.
El ejemplo de Chris lo demuestra muy bien:
Cómo construiremos el gráfico
Para crear los segmentos del gráfico de anillos, haremos un círculo separado para cada uno, superpondremos los círculos uno encima del otro, luego usaremos stroke
, stroke-dasharray
y stroke-dashoffset
para mostrar solo una parte del trazo de cada círculo. Luego rotaremos cada parte visible a la posición correcta, creando la ilusión de una sola forma. Mientras hacemos esto, también calcularemos las coordenadas de las etiquetas de texto.
Aquí hay un ejemplo que demuestra estas rotaciones y superposiciones:
Configuración básica
Comenzamos configurando nuestra estructura. Estoy usando x-template para fines de demostración, pero recomendaría crear un componente de archivo único para producción.
div donut-chart/donut-chart/divscript type="text/x-template" svg viewBox="0 0 160 160" g v-for="(value, index) in initialValues" circle :cx="cx" :cy="cy" :r="radius" fill="transparent" :stroke="colors[index]" :stroke-width="strokeWidth" /circle text/text /g /svg/script
Vue.component('donutChart', { template: '#donutTemplate', props: ["initialValues"], data() { return { chartData: [], colors: ["#6495ED", "goldenrod", "#cd5c5c", "thistle", "lightgray"], cx: 80, cy: 80, radius: 60, sortedValues: [], strokeWidth: 30, } } })new Vue({ el: "#app", data() { return { values: [230, 308, 520, 130, 200] } },});
Con esto nosotros:
- Cree nuestra instancia de Vue y nuestro componente de gráfico de anillos, luego dígale a nuestro componente de anillos que espere algunos valores (nuestro conjunto de datos) como accesorios.
- Establecer nuestras formas SVG básicas:
para los segmentos y
para las etiquetas, con las dimensiones básicas, ancho de trazo y colores definidos - Envuelva estas formas en un
elemento, que las agrupa - Agregue un
v-for
bucle alg
elemento, que usaremos para iterar a través de cada valor que recibe el componente. - Cree una
sortedValues
matriz vacía, que usaremos para contener una versión ordenada de nuestros datos. - Cree una
chartData
matriz vacía, que contendrá nuestros datos de posicionamiento principal.
Longitud del círculo
Nuestra stroke-dasharray
debería ser la longitud de todo el círculo, lo que nos dará un número de referencia sencillo que podemos usar para calcular cada stroke-dashoffset
valor. Recuerda que la longitud de un círculo es su circunferencia y la fórmula para calcular la circunferencia es 2πr (recuerda esto, ¿verdad?).
Podemos hacer de esto una propiedad calculada en nuestro componente.
computed: { circumference() { return 2 * Math.PI * this.radius }}
…y vincular el valor a nuestro marcado de plantilla.
svg viewBox="0 0 160 160" g v-for="(value, index) in initialValues" circle :cx="cx" :cy="cy" :r="radius" fill="transparent" :stroke="colors[index]" :stroke-width="strokeWidth" :stroke-dasharray="circumference"/circle text/text /g/svg
En la maqueta inicial, vemos que los segmentos iban de mayor a menor. Podemos crear otra propiedad calculada para ordenarlos. Almacenaremos la versión ordenada dentro de la sortedValues
matriz.
sortInitialValues() { return this.sortedValues = this.initialValues.sort((a,b) = b-a)}
Finalmente, para que estos valores ordenados estén disponibles para Vue antes de que se represente el gráfico, querremos hacer referencia a esta propiedad calculada desde el mounted()
enlace del ciclo de vida.
mounted() { this.sortInitialValues }
En este momento, nuestro gráfico se ve así:
Sin segmentos. Solo una dona de color sólido. Al igual que HTML, los elementos SVG se representan en el orden en que aparecen en el marcado. El color que aparece es el color del trazo del último círculo en el SVG. Como stroke-dashoffset
aún no hemos agregado ningún valor, el trazo de cada círculo recorre todo el contorno. Arreglemos esto creando segmentos.
Creando segmentos
Para obtener cada uno de los segmentos del círculo, necesitaremos:
- Calcule el porcentaje de cada valor de datos a partir de los valores de datos totales que pasamos
- Multiplica este porcentaje por la circunferencia para obtener la longitud del trazo visible.
- Resta esta longitud de la circunferencia para obtener el
stroke-offset
Suena más complicado de lo que es. Comenzamos con algunas funciones auxiliares. Primero necesitamos sumar los valores de nuestros datos. Podemos usar una propiedad calculada para hacer esto.
dataTotal() { return this.sortedValues.reduce((acc, val) = acc + val)},
Para calcular el porcentaje de cada valor de datos, necesitaremos pasar valores del v-for
bucle que creamos anteriormente, lo que significa que necesitaremos agregar un método.
methods: { dataPercentage(dataVal) { return dataVal / this.dataTotal }},
Ahora tenemos suficiente información para calcular nuestros stroke-offset
valores, que establecerán nuestros segmentos circulares.
Nuevamente, queremos: (a) multiplicar nuestro porcentaje de datos por la circunferencia del círculo para obtener la longitud del trazo visible, y (b) restablecer esta longitud de la circunferencia para obtener el stroke-offset
.
Este es el método para obtener nuestros stroke-offset
s:
calculateStrokeDashOffset(dataVal, circumference) { const strokeDiff = this.dataPercentage(dataVal) * circumference return circumference - strokeDiff},
…que vinculamos a nuestro círculo en el HTML con:
:stroke-dashoffset="calculateStrokeDashOffset(value, circumference)"
¡Y voilá! Deberíamos tener algo como esto:
Segmentos rotatorios
Ahora la parte divertida. Todos los segmentos comienzan a las 3 en punto, que es el punto de partida predeterminado para los círculos SVG. Para colocarlos en el lugar correcto, debemos rotar cada segmento a su posición correcta.
Podemos hacer esto encontrando la proporción de cada segmento en 360 grados y luego compensando esa cantidad con el total de grados anteriores.
Primero, agreguemos una propiedad de datos para realizar un seguimiento del desplazamiento:
angleOffset: -90,
Entonces nuestro cálculo (esta es una propiedad calculada):
calculateChartData() { this.sortedValues.forEach((dataVal, index) = { const data = { degrees: this.angleOffset, } this.chartData.push(data) this.angleOffset = this.dataPercentage(dataVal) * 360 + this.angleOffset })},
Cada bucle crea un nuevo objeto con una propiedad “grados”, lo inserta en nuestra chartValues
matriz que creamos anteriormente y luego lo actualiza angleOffset
para el siguiente bucle.
Pero espera, ¿qué pasa con el valor -90?
Bueno, volviendo a nuestra maqueta original, el primer segmento se muestra en la posición de las 12 en punto, o -90 grados desde el punto de partida. Al establecer nuestro angleOffset
-90, nos aseguramos de que nuestro segmento de donas más grande comience desde arriba.
Para rotar estos segmentos en HTML, usaremos el atributo de presentación de transformación con la rotate
función. Creemos otra propiedad calculada para que podamos devolver una cadena agradable y formateada.
returnCircleTransformValue(index) { return `rotate(${this.chartData[index].degrees}, ${this.cx}, ${this.cy})`},
La rotate
función toma tres argumentos: un ángulo de rotación y las coordenadas xey alrededor de las cuales gira el ángulo. Si no obtenemos las coordenadas cx y cy, nuestros segmentos rotarán alrededor de todo el sistema de coordenadas SVG.
A continuación, vinculamos esto a nuestra circular marcada.
:transform="returnCircleTransformValue(index)"
Y, dado que necesitamos hacer todos estos cálculos antes de representar el gráfico, agregaremos nuestra calculateChartData
propiedad calculada en el gancho montado:
mounted() { this.sortInitialValues this.calculateChartData}
Finalmente, si queremos ese dulce, dulce espacio entre cada segmento, podemos restablecer dos a la circunferencia y usar esto como nuestro nuevo stroke-dasharray
.
adjustedCircumference() { return this.circumference - 2},
:stroke-dasharray="adjustedCircumference"
¡Segmentos, cariño!
Etiquetas
Tenemos nuestros segmentos, pero ahora necesitamos crear etiquetas. Esto significa que necesitamos colocar nuestros
elementos con coordenadas xey en diferentes puntos a lo largo del círculo. Podrías sospechar que esto requiere matemáticas. Lamentablemente, tienes razón.
Afortunadamente, este no es el tipo de matemáticas en el que necesitamos aplicar conceptos reales; Este es más bien del tipo en el que buscamos fórmulas en Google y no hacemos demasiadas preguntas.
Según Internet, las fórmulas para calcular los puntos xeya lo largo de un círculo son:
x = r cos(t) + ay = r sin(t) + b
…donde r
está el radio, t
es el ángulo, y a
y b
son los desplazamientos de los puntos centrales xe y.
Ya tenemos la mayor parte de esto: conocemos nuestra radio, sabemos cómo calcular los ángulos de nuestro segmento y conocemos nuestros valores de desplazamiento central (cx y cy).
Sin embargo, hay un inconveniente: en esas fórmulas, t
está en *radianes*. Estamos trabajando en grados, lo que significa que necesitamos hacer algunas conversiones. Nuevamente, una búsqueda rápida arroja una fórmula:
radians = degrees * (π / 180)
…que podemos representar en un método:
degreesToRadians(angle) { return angle * (Math.PI / 180)},
Ahora tenemos suficiente información para calcular nuestras coordenadas de texto xey:
calculateTextCoords(dataVal, angleOffset) { const angle = (this.dataPercentage(dataVal) * 360) / 2 + angleOffset const radians = this.degreesToRadians(angle) const textCoords = { x: (this.radius * Math.cos(radians) + this.cx), y: (this.radius * Math.sin(radians) + this.cy) } return textCoords},
Primero, calculamos el ángulo de nuestro segmento multiplicando la relación de nuestro valor de datos por 360; Sin embargo, en realidad queremos la mitad de esto porque nuestras etiquetas de texto están en el medio del segmento en lugar del final. Necesitamos agregar el desplazamiento del ángulo como lo hicimos cuando creamos los segmentos.
Nuestro calculateTextCoords
método ahora se puede utilizar en la calculateChartData
propiedad calculada:
calculateChartData() { this.sortedValues.forEach((dataVal, index) = { const { x, y } = this.calculateTextCoords(dataVal, this.angleOffset) const data = { degrees: this.angleOffset, textX: x, textY: y } this.chartData.push(data) this.angleOffset = this.dataPercentage(dataVal) * 360 + this.angleOffset })},
Agreguemos también un método para devolver la cadena de etiqueta:
percentageLabel(dataVal) { return `${Math.round(this.dataPercentage(dataVal) * 100)}%`},
Y, en el marcado:
text :x="chartData[index].textX" :y="chartData[index].textY"{{ percentageLabel(value) }}/text
Ahora tenemos etiquetas:
Blech, está tan descentrado. Podemos solucionarlo con el atributo de presentación text-anchor. Según la fuente y el tamaño font-size
, es posible que también quieras ajustar la posición. Consulta dx y dy para esto.
Elemento de texto renovado:
text text-anchor="middle" dy="3px" :x="chartData[index].textX" :y="chartData[index].textY"{{ percentageLabel(value) }}/text
Mmm, parece que si tenemos porcentajes pequeños, las etiquetas quedan fuera de los segmentos. Agreguemos un método para comprobarlo.
segmentBigEnough(dataVal) { return Math.round(this.dataPercentage(dataVal) * 100) 5}
text v-if="segmentBigEnough(value)" text-anchor="middle" dy="3px" :x="chartData[index].textX" :y="chartData[index].textY"{{ percentageLabel(value) }}/text
Ahora, solo agregaremos etiquetas a segmentos mayores al 5%.
¡Y ya está! Ahora tenemos un componente de gráfico de anillos reutilizable que puede aceptar cualquier conjunto de valores y crear segmentos. ¡Genial!
El producto terminado:
Próximos pasos
Hay muchas maneras en que podemos modificar o mejorar esto ahora que está construido. Por ejemplo:
- Agregar elementos para mejorar la accesibilidad , como etiquetas
title
ydesc
, etiquetas aria y atributos de rol aria. - Crea animaciones con CSS o bibliotecas como Greensock para crear efectos llamativos cuando aparece el gráfico.
- Jugando con combinaciones de colores .
/code and code markup="tt"desc/code tags, aria-labels, and aria role attributes./li liCreating stronganimations/strong with CSS or libraries like a href="https://greensock.com/"Greensock/a to create eye-catching effects when the chart comes into view./li liPlaying with strongcolor schemes/strong./li /ul pI’d love to hear what you think about this implementation and other experiences you’ve had with SVG charts. Share in the comments!/p
Me encantaría saber qué piensa sobre esta implementación y otras experiencias que ha tenido con los gráficos SVG. ¡Comparte en los comentarios!
Deja una respuesta