Otro intento con el gráfico circular CSS perfecto… ¡sin JavaScript!
reciente, Juan Diego Rodríguez publicó un gran artículo. Explore hasta dónde puede llegar CSS para crear gráficos circulares semánticos y personalizables manteniendo JavaScript al mínimo.
Para citar al propio Juan:
En este artículo, intentaremos crear un gráfico circular perfecto usando CSS. Esto significa evitar JavaScript tanto como sea posible mientras se resuelven los principales problemas que plantean los gráficos circulares escritos a mano.
Enumera algunos objetivos que me gustaría volver a lograr en orden de prioridad:
- ¡Esto debe ser semántico! Esto significa que los lectores de pantalla deberían poder comprender los datos que se muestran en el gráfico circular.
Hasta donde tengo entendido, la solución del artículo original logra ese objetivo. Su enfoque semántico (etiquetas en HTML simple + valores como atributos reinyectados en el DOM a través de pseudoelementos) es limpio, expresivo y, con suerte, de fácil acceso.
- ¡Esto debería ser HTML personalizable! Una vez que se completa el CSS, podemos personalizar el gráfico circular simplemente cambiando el marcado.
El artículo original también logró este objetivo.
- ¡Esto debería mantener JavaScript al mínimo! En términos generales, JavaScript no tiene nada de malo, simplemente es más divertido de esta manera.
El artículo original fue diseñado para utilizar la menor cantidad de JavaScript posible, principalmente por diversión. Tiendo a estar un poco en desacuerdo. Para mí, esto no debería ser sólo por diversión porque…
- JavaScript se utiliza para manejar el estado y la lógica, y
- CSS se utiliza para diseñar el marcado.
La restricción original de “no JavaScript” tenía sentido para mí. CSS debería ser lo suficientemente potente como para permitirnos diseñar un gráfico circular. JavaScript no debería ser necesario. Entonces, decidí ver si había una manera de deshacerme de él al 100% y, solo por diversión, lo bifurqué. Artículo CodePen Durante la pausa del almuerzo.
Mantuve el código fuente lo más inalterado posible, preservando su semántica y personalización del lado HTML. Si no está roto, no lo arregles™.
Casualmente, este artículo está en Recientemente usé un bolígrafo corto para jugar con gráficos de barras.. Entonces ya quiero ver el gráfico. Pero los gráficos de barras son mucho más sencillos: la posición o el tamaño de cada barra no depende de las otras barras. Los gráficos circulares son una bestia diferente: la posición de cada sector depende del sector anterior. Afortunadamente, esto lo convierte en un desafío más interesante.
Pero antes de profundizar en mis pensamientos sobre los gráficos circulares, echemos un vistazo a cómo otros desarrolladores web abordan estos gráficos circulares.
tecnología existente
Leo muchos blogs, artículos y ejemplos de código de desarrolladores front-end profesionales, pero yo no soy uno de ellos, por lo que no estoy del todo seguro de tener la capacidad de identificar las tecnologías existentes más recientes y relevantes… De todos modos, intentémoslo.
Es fácil encontrar muchas bibliotecas de JavaScript para trabajar con gráficos. Los uso mucho en el trabajo. Sin embargo, debido a que no tenemos restricciones de JavaScript, las excluiremos.
Empecé a buscar un gráfico circular CSS puro y la primera biblioteca que apareció fue Gráfico CSS. Promueve la estructura semántica, las etiquetas HTML utilizadas para mostrar el material, la accesibilidad y el material original dentro de las etiquetas. Parece ser una biblioteca muy buena y no utiliza JavaScript.
En su lugar, utiliza tablas HTML, lo que en mi opinión y experiencia tiene mucho sentido (la mayoría de las veces, el material fuente está en una tabla). Sin embargo, no resuelve el desafío específico de permitir que el usuario solo establezca valores mientras calcula automáticamente los ángulos inicial y final de cada corte. En este caso, el usuario aún necesita definirlos manualmente.
También hay algunos artículos muy buenos que tratan sobre gráficos o visualización de datos en general. Sólo por nombrar algunos:
Sólo tienen un pequeño inconveniente (pero para nosotros muy importante). Si bien estos recursos son valiosos para explicar cómo funciona la accesibilidad a los gráficos, en realidad no abordan “interfaces” HTML simples o implementaciones CSS puras.
cómo resolví este problema
Si todavía estás leyendo, supongo que al menos estás algo interesado en mi enfoque. Es comprensible que si solo quieres ver el código, ¡aquí lo tienes!
Inicialmente, la razón por la que se necesita JavaScript es que cada segmento necesita conocer el valor del segmento anterior. Sin embargo, debido a cómo funciona la herencia de propiedades CSS, un niño no tiene forma de conocer el estado de otro niño. A pesar de saber esto, primero intenté determinar si existía una técnica especializada o de “brujería” que me permitiera eliminar JavaScript manteniendo el marcado HTML original y los métodos basados en atributos.
sé que a la gente le gusta Roman Komárov Se pueden hacer cosas increíbles con CSS, por lo que incluso he considerado explorar técnicas que involucran animación de propiedades. Pero obviamente no tuve tiempo de investigar en esa dirección.
Vuelvo al problema central: debido a cómo funciona la herencia CSS, un niño no tiene forma de conocer el estado de sus hermanos. Obviamente necesito una “entidad circundante” para manejar esto.
En la publicación de Juan, esta “entidad” es JavaScript, que recorre todos los subelementos y calcula la acumulación de sectores adecuada.
const pieChartItems = document.querySelectorAll(".pie-chart li");
let accum = 0;
pieChartItems.forEach((item) => {
item.style.setProperty("--accum", accum);
accum += parseFloat(item.getAttribute("data-percentage"));
});
El código JavaScript establece un --accum El valor de cada sector, que contiene el valor porcentual de todos los gráficos anteriores. Sin él, no sabríamos la ubicación de cada porción y su etiqueta correspondiente.
En HTML/CSS, esta entidad también existe: el clásico elemento padre. Entonces mi solución fue mover el valor porcentual al padre.
Primero, recordemos que el marcado original de un gráfico circular se ve así:
<ul class="pie-chart">
<li data-percentage-1="10">Apple</li>
<li data-percentage-2="30">Banana</li>
<li data-percentage-3="20">Orange</li>
<li data-percentage-4="40">Strawberry</li>
</ul>
La versión que usaremos se ve así:
<ul class="pie-chart" data-percentage-1="10" data-percentage-2="30" data-percentage-3="20" data-percentage-4="40">
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
<li>Strawberry</li>
</ul>
Hemos movido todos los valores al padre. <ul> Y asigne a cada elemento un nombre único, indexándolos de manera efectiva.
Por ejemplo, he probado esta solución CSS “indexada” antes para compensar la falta de sibling-index() y sibling-count() Función generar números aleatorios. Sé que esta es la dirección correcta y el resto seguirá naturalmente en términos de CSS.
Spoilers: sibling-index() y sibling-count() ¡A punto de convertirse en base!
Puede parecer un duplicado porque no agregamos nada pero movimos las propiedades. Sin embargo, este pequeño cambio nos permite administrar todas las etiquetas y valores del padre en CSS. Lo mejor es que aún mantenemos todas las propiedades juntas. Si bien se podría argumentar que esto tampoco se escala, si tenemos datos con una gran cantidad de entradas, un gráfico circular rara vez es la mejor opción para mostrarlos.
Alternativamente, podemos agregar data-label Las propiedades de la etiqueta son solo para emparejar visualmente la etiqueta y el valor.
<ul class="pie-chart" data-percentage-1="10" data-percentage-2="30" data-percentage-3="20" data-percentage-4="40">
<!-- Optional data-label attributes: just visual hints-->
<li data-label-1>Apple</li>
<li data-label-2>Banana</li>
<li data-label-3>Orange</li>
<li data-label-4>Strawberry</li>
</ul>
Ahora revisemos el CSS. Esta implementación requiere dos conjuntos de reglas CSS repetitivas pero simples.
Primero, debemos pasar cada porcentaje a su porción correspondiente. Para esto usamos Juan y obtenemos data-percentage Actualizando propiedades a CSS attr() Función. Al mismo tiempo los asignaremos a los cortes correspondientes usando: nth-child() selector.
.pie-chart {
/* We write one for each slice we think we'll need */
--p-100-1: attr(data-percentage-1 type(<number>)); :nth-child(1) { --p-100: var(--p-100-1) }
--p-100-2: attr(data-percentage-2 type(<number>)); :nth-child(2) { --p-100: var(--p-100-2) }
--p-100-3: attr(data-percentage-3 type(<number>)); :nth-child(3) { --p-100: var(--p-100-3) }
--p-100-4: attr(data-percentage-4 type(<number>)); :nth-child(4) { --p-100: var(--p-100-4) }
/*...*/
}
Para ese tipo de código repetitivo/incremental, lo mantengo como una sola línea de código sin automóvil. En mi humilde opinión Esta es una excepción muy aceptable a las reglas de formato comunes, ya que evita errores tipográficos al simplificar las capacidades de escaneo y también simplifica la iteración adicional (por ejemplo, agregando soporte para más sectores). Pero su kilometraje puede variar.
Echemos un vistazo más de cerca a lo que está pasando aquí. A nivel de todo el gráfico circular, accedemos a los porcentajes de cada sector a través del índice y los almacenamos en las variables CSS correspondientes, por lo que el cuarto elemento obtiene --p-100-4el quinto elemento obtiene --p-100-5etc:
--p-100-4: attr(data-percentage-4 type(<number>));
A continuación, repasamos cada --p-100 Variables locales para cada segmento.
:nth-child(4) {
--p-100: var(--p-100-4);
}
Ahora, podemos acceder a todos estos valores de corte en dos niveles:
- En un gráfico circular, por variable de índice:
--p-100-1,--p-100-2,--p-100-3 - En cada rebanada, pase
--p-100cambiable
Ahora necesitamos calcular el correspondiente. --accum valor, que es la suma de los valores de todos los sectores anteriores. Para hacer esto tenemos que sumar paso a paso cada porcentaje después de cada porción y luego asignar los valores a las porciones usando nth-child() de nuevo.
.pie-chart {
/* ... */
--accum-1: 0; :nth-child(1) { --accum: var(--accum-1) }
--accum-2: calc(var(--accum-1) + var(--p-100-1)); :nth-child(2) { --accum: var(--accum-2) }
--accum-3: calc(var(--accum-2) + var(--p-100-2)); :nth-child(3) { --accum: var(--accum-3) }
--accum-4: calc(var(--accum-3) + var(--p-100-3)); :nth-child(4) { --accum: var(--accum-4) }
/*...*/
}
Nuevamente, trabajamos primero en el nivel del gráfico circular, donde calculamos una variable dedicada para cada sector. El primer segmento es un caso especial: no existe un segmento anterior, por lo que la acumulación es 0. Durante el reposo, la acumulación de rodajas n es la acumulación de porciones anteriores n−1 Añade el valor de la porción. n−1.
--accum-4: calc(var(--accum-3) + var(--p-100-3));
El cuarto elemento obtiene --accum-4el quinto elemento obtiene --accum-5etc. Al igual que los porcentajes, a nivel de cada segmento los asignamos a variables locales. --accum.
:nth-child(4) {
--accum: var(--accum-4);
}
Nuevamente, podemos acceder a todos estos valores de acumulación de sectores en dos niveles:
- En un gráfico circular, por variable de índice:
--accum-1,--accum-2,--accum-3 - En cada rebanada, pase
--accumcambiable
Espero tener funcionalidad CSS nativa en el futuro (tal vez @function? ) evitará que tengamos que recurrir a esta duplicación de código. Al mismo tiempo, puedes utilizar preprocesadores CSS (Sass, Less) para simplificar este proceso.
Al bifurcar el Pen original, me vinieron a la cabeza algunas preguntas (que en realidad no exploré) para mantener el código original lo más inalterado posible:
- como usarlo
<label>y<meter>¿Para etiquetas y valores? - ¿Qué tal usar uno?
<table>(Dado que los gráficos generalmente se extraen de tablas que contienen filas, p. ej.(label, value))?
Notas sobre accesibilidad
En mi bifurcación, manejo la accesibilidad de la misma manera que Juan, pero con una ligera modificación: uso counter-reset / counter() en lugar de attr() Asignar porcentaje a content propiedad. Esto debería ser igual que attr()pero asegurémonos de que siga siendo adecuado para lectores de pantalla:
Otra cosa que quiero cambiar es el elemento de etiqueta dentro de cada <li>. En el artículo original, Juan usó <strong> elemento, y elegí <span> en cambio. Sin embargo, creo que usar <label> sí mismo. Generalmente pensamos que están confinados dentro <form> elemento, pero teoría normativa Podemos esperar usarlos en contextos donde se requiere contenido de redacción. Entonces no encuentro ninguna obligación de usarlos solo en formularios.
Color predeterminado
El artículo de Juan también pide algunas mejorasintenté resolver este problema en mi bifurcación:
data-colorSe puede omitir y se producirá el color.- Los colores se pueden definir en el padre o el hijo (elección del usuario; ambos son compatibles).
Esto se convertirá al siguiente fragmento de cada segmento:
.pie-chart li {
--color: attr(data-color type(<color>));
--bg-color: var(--color, hsl(calc(360deg * sibling-index() / sibling-count()) 90% 40%));
}
no usé sibling-index() y sibling-count() Principalmente porque aún no son líneas base (¡todavía no, pero pronto!), pero no puedo detenerme porque calcular sus tonos es muy sofisticado. Estas características permiten algunas magia!
Sin embargo, aquí está mi “relleno CSS puro”; código repetido (pero simple):
.pie-chart {
:has(:nth-child(1)) { --sibling-count: 1 } :nth-child(1) { --sibling-index: 1; }
:has(:nth-child(2)) { --sibling-count: 2 } :nth-child(2) { --sibling-index: 2; }
:has(:nth-child(3)) { --sibling-count: 3 } :nth-child(3) { --sibling-index: 3; }
:has(:nth-child(4)) { --sibling-count: 4 } :nth-child(4) { --sibling-index: 4; }
/* ... */
}
¿Más tipos de gráficos?
Ahora tenemos una base común para otros tipos de gráficos. Como prueba de concepto, implementé el patrón de gráfico de barras. mi tenedor.
¿Quizás un elemento web?
En cierto modo, ya tenemos un elemento web, uno sin JavaScript, que usa DOM ligero.
para mí <pie-char attributes...> ninguna diferencia esencial <div class="pie chart" attributes...> o <div pie chart attributes...>. Sin embargo, puedo ver el valor de este enfoque al considerar la mejora progresiva.
Por ejemplo, gráficos que se actualizan automáticamente y obtienen datos instantáneos. Pero esto requiere JavaScript, que hoy evitamos deliberadamente.