Cómo crear un gráfico animado de cuadrados anidados usando máscaras | Programar Plus

Tenemos muchos tipos de gráficos conocidos: barra, anillo, línea, pastel, lo que sea. Todas las bibliotecas de gráficos populares los admiten. Luego están los tipos de gráficos que ni siquiera tienen un nombre. Mire este gráfico soñado con cuadrados apilados (anidados) que pueden ayudar a visualizar los tamaños relativos, o cómo se comparan los diferentes valores entre sí:

Lo que estamos haciendo

Sin ninguna interactividad, crear este diseño es bastante sencillo. Una forma de hacerlo es apilar elementos (por ejemplo, SVG <rect> elementos, o incluso divs HTML) en tamaños decrecientes, donde todas sus esquinas inferiores izquierdas tocan el mismo punto.

Pero las cosas se complican una vez que introducimos algo de interactividad. Así es como debería ser: cuando movemos el mouse sobre una de las formas, queremos que las otras desaparezcan y se alejen.

Crearemos estas formas irregulares usando rectángulos y máscaras — literal <svg> con <rect> y <mask> elementos. Si eres completamente nuevo en las máscaras, estás en el lugar correcto. Este es un artículo de nivel introductorio. Si eres más curtido, quizás este efecto cut-out sea un truco que puedas llevar contigo.

Ahora, antes de comenzar, es posible que se pregunte si hay una mejor alternativa a SVG que el uso de formas personalizadas. ¡Esa es definitivamente una posibilidad! Pero dibujar formas con un <path> puede ser intimidante o incluso desordenado. Entonces, estamos trabajando con elementos “más fáciles” para obtener las mismas formas y efectos.

Por ejemplo, así es como tendríamos que representar la forma azul más grande usando un <path>.

<svg viewBox="0 0 320 320" width="320" height="320">
  <path d="M320 0H0V56H264V320H320V0Z" fill="#264653"/>
</svg>

Si el 0H0V56… no tiene ningún sentido para usted, consulte “El SVG path Syntax: An Illustrated Guide” para obtener una explicación detallada de la sintaxis.

Los fundamentos de la carta

Dado un conjunto de datos como este:

type DataSetEntry = {
  label: string;
  value: number;
};

type DataSet = DataSetEntry[];

const rawDataSet: DataSet = [
  { label: 'Bad', value: 1231 },
  { label: 'Beginning', value: 6321 },
  { label: 'Developing', value: 10028 },
  { label: 'Accomplished', value: 12123 },
  { label: 'Exemplary', value: 2120 }
];

…queremos terminar con un SVG como este:

<svg viewBox="0 0 320 320" width="320" height="320">
  <rect width="320" height="320" y="0" fill="..."></rect>
  <rect width="264" height="264" y="56" fill="..."></rect>
  <rect width="167" height="167" y="153" fill="..."></rect>
  <rect width="56" height="56" y="264" fill="..."></rect>
  <rect width="32" height="32" y="288" fill="..."></rect>
</svg>

Determinación del valor más alto

En un momento se hará evidente por qué necesitamos el valor más alto. Podemos usar el Math.max() para conseguirlo. Acepta cualquier número de argumentos y devuelve el valor más alto de un conjunto.

const dataSetHighestValue: number = Math.max(
  ...rawDataSet.map((entry: DataSetEntry) => entry.value)
);

Dado que tenemos un pequeño conjunto de datos, podemos decir que obtendremos 12123.

Cálculo de la dimensión de los rectángulos.

Si nos fijamos en el diseño, el rectángulo que representa el valor más alto (12123) cubre toda el área del gráfico.

Elegimos arbitrariamente 320 para las dimensiones SVG. Como nuestros rectángulos son cuadrados, el ancho y la altura son iguales. ¿Cómo podemos hacer 12123 igual a 320? ¿Qué hay de los valores menos “especiales”? ¿Qué tan grande es el 6321 ¿rectángulo?

Preguntado de otra manera, ¿cómo mapeamos un número de un rango ([0, 12123]) a otro ([0, 320])? O, en términos más matemáticos, ¿cómo escalamos una variable a un intervalo de [a, b]?

Para nuestros propósitos, vamos a implementar la función de esta manera:

const remapValue = (
  value: number,
  fromMin: number,
  fromMax: number,
  toMin: number,
  toMax: number
): number => {
  return ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin) + toMin;
};

remapValue(1231, 0, 12123, 0, 320); // 32
remapValue(6321, 0, 12123, 0, 320); // 167
remapValue(12123, 0, 12123, 0, 320); // 320

Dado que asignamos valores al mismo rango en nuestro código, en lugar de pasar los mínimos y máximos una y otra vez, podemos crear una función contenedora:

const valueRemapper = (
  fromMin: number,
  fromMax: number,
  toMin: number,
  toMax: number
) => {
  return (value: number): number => {
    return remapValue(value, fromMin, fromMax, toMin, toMax);
  };
};

const remapDataSetValueToSvgDimension = valueRemapper(
  0,
  dataSetHighestValue,
  0,
  svgDimension
);

Podemos usarlo así:

remapDataSetValueToSvgDimension(1231); // 32
remapDataSetValueToSvgDimension(6321); // 167
remapDataSetValueToSvgDimension(12123); // 320

Crear e insertar los elementos DOM

Lo que queda tiene que ver con la manipulación del DOM. Tenemos que crear el <svg> y los cinco <rect> elementos, establezca sus atributos y agréguelos al DOM. Todo esto lo podemos hacer con lo básico. createElementNS, setAttribute, y el appendChild funciones.

Note que estamos usando el createElementNS en lugar de los más comunes createElement. Esto se debe a que estamos trabajando con un SVG. Los elementos HTML y SVG tienen especificaciones diferentes, por lo que pertenecen a un URI de espacio de nombres diferente. Simplemente sucede que el createElement utiliza convenientemente el espacio de nombres HTML! Entonces, para crear un SVG, tenemos que ser así de detallados:

document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;

Seguramente, podemos crear otra función auxiliar:

const createSvgNSElement = (element: string): SVGElement => {
  return document.createElementNS('http://www.w3.org/2000/svg', element);
};

Cuando agregamos los rectángulos al DOM, debemos prestar atención a su orden. De lo contrario, tendríamos que especificar el z-index explícitamente. El primer rectángulo tiene que ser el más grande y el último rectángulo tiene que ser el más pequeño. Lo mejor es ordenar los datos antes del bucle.

const data = rawDataSet.sort(
  (a: DataSetEntry, b: DataSetEntry) => b.value - a.value
);

data.forEach((d: DataSetEntry, index: number) => {
  const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;
  const rectDimension: number = remapDataSetValueToSvgDimension(d.value);

  rect.setAttribute('width', `${rectDimension}`);
  rect.setAttribute('height', `${rectDimension}`);
  rect.setAttribute('y', `${svgDimension - rectDimension}`);

  svg.appendChild(rect);
});

El sistema de coordenadas comienza desde la parte superior izquierda; ahí es donde el [0, 0] es. Siempre vamos a dibujar los rectángulos desde el lado izquierdo. El x atributo, que controla la posición horizontal, por defecto es 0, por lo que no tenemos que configurarlo. El y El atributo controla la posición vertical.

Para dar la impresión visual de que todos los rectángulos se originan en el mismo punto que toca sus esquinas inferiores izquierdas, tenemos que empujar los rectángulos hacia abajo, por así decirlo. ¿Por cuanto? La cantidad exacta que el rectángulo no llena. Y ese valor es la diferencia entre la dimensión del gráfico y el rectángulo en particular. Si ponemos todos los bits juntos, terminamos con esto:

Ya agregamos el código para la animación a esta demostración usando CSS.

Rectángulos recortados

Tenemos que convertir nuestros rectángulos en formas irregulares que parezcan el número siete o la letra L girada 180 grados.

Si nos enfocamos en las “partes que faltan”, podemos ver los recortes de los mismos rectángulos con los que ya estamos trabajando.

Queremos ocultar esos recortes. Así es como vamos a terminar con las formas de L que queremos.

Enmascaramiento 101

Una máscara es algo que defines y luego aplicas a un elemento. Típicamente, el mask está incrustado en el <svg> elemento al que pertenece. Y, en general, debe tener un único id porque tenemos que referenciarlo para aplicar la máscara a un elemento.

<svg>
  <mask id="...">
    <!-- ... -->
  </mask>
</svg>

En el <mask> etiqueta, ponemos las formas que sirven como las máscaras reales. También aplicamos el mask atribuye a los elementos.

<svg>
  <mask id="myCleverlyNamedMask">
    <!-- ... -->
  </mask>
  <rect mask="url(#myCleverlyNamedMask)"></rect>
</svg>

Esa no es la única forma de definir o aplicar una máscara, pero es la forma más directa para esta demostración. Hagamos un poco de experimentación antes de escribir cualquier código para generar las máscaras.

Dijimos que queremos cubrir las áreas recortadas que coincidan con los tamaños de los rectángulos existentes. Si tomamos el elemento más grande y le aplicamos el rectángulo anterior como máscara, terminamos con este código:

<svg viewBox="0 0 320 320" width="320" height="320">
  <mask id="theMask">
    <rect width="264" height="264" y="56" fill=""></rect>
  </mask>
  <rect width="320" height="320" y="0" fill="#264653" mask="url(#theMask)"></rect>
</svg>

El elemento dentro de la máscara necesita un fill valor. ¿Qué debería ser eso? Veremos resultados completamente diferentes basados ​​en el fill valor (color) que elijamos.

el relleno blanco

Si usamos un white valor para el fill, entonces obtenemos esto:

Ahora, nuestro rectángulo grande tiene la misma dimensión que el rectángulo de enmascaramiento. No es exactamente lo que queríamos.

el relleno negro

Si usamos un black valor en su lugar, entonces se ve así:

No vemos nada. Eso es porque lo que se llena de negro es lo que se vuelve invisible. Controlamos la visibilidad de las máscaras usando white y black llena Las líneas discontinuas están ahí como una ayuda visual para hacer referencia a las dimensiones del área invisible.

el relleno gris

Ahora usemos algo intermedio entre blanco y negro, digamos gray:

No es completamente opaco ni sólido; es transparente Entonces, ahora sabemos que podemos controlar el “grado de visibilidad” aquí usando algo diferente a white y black valores que es un buen truco para guardar en nuestros bolsillos traseros.

el último bit

Esto es lo que hemos cubierto y aprendido sobre las máscaras hasta ahora:

  • El elemento dentro del <mask> controla la dimensión del área enmascarada.
  • Podemos hacer que el contenido del área enmascarada sea visible, invisible o transparente.

Solo hemos usado una forma para la máscara, pero como con cualquier etiqueta HTML de uso general, podemos anidar tantos elementos secundarios como queramos. De hecho, el truco para conseguir lo que queremos es utilizar dos SVG <rect> elementos. Tenemos que apilarlos uno encima del otro:

<svg viewBox="0 0 320 320" width="320" height="320">
  <mask id="maskW320">
    <rect width="320" height="320" y="0" fill="???"></rect>
    <rect width="264" height="264" y="56" fill="???"></rect>
  </mask>
  <rect width="320" height="320" y="0" fill="#264653" mask="url(#maskW320)"></rect>
</svg>

Uno de nuestros rectángulos de enmascaramiento está lleno de white; el otro esta lleno de black. Incluso si conocemos las reglas, probemos las posibilidades.

<mask id="maskW320">
  <rect width="320" height="320" y="0" fill="black"></rect>
  <rect width="264" height="264" y="56" fill="white"></rect>
</mask>

El <mask> es la dimensión del elemento más grande y el elemento más grande está lleno de black. Eso significa que todo debajo de esa área es invisible. Y todo lo que está debajo del rectángulo más pequeño es visible.

Ahora vamos a hacer voltear las cosas donde el black el rectángulo está en la parte superior:

<mask id="maskW320">
  <rect width="320" height="320" y="0" fill="white"></rect>
  <rect width="264" height="264" y="56" fill="black"></rect>
</mask>

¡Esto es lo que queremos!

Todo lo que está debajo del rectángulo blanco más grande es visible, pero el rectángulo negro más pequeño está encima (más cerca de nosotros en el eje z), enmascarando esa parte.

Generando las máscaras

Ahora que sabemos lo que tenemos que hacer, podemos crear las máscaras con relativa facilidad. Es similar a cómo generamos los rectángulos de colores en primer lugar: creamos un ciclo secundario donde creamos el mask y los dos rects.

Esta vez, en lugar de agregar el rects directamente al SVG, lo agregamos al mask:

data.forEach((d: DataSetEntry, index: number) => {
  const mask: SVGMaskElement = createSvgNSElement('mask') as SVGMaskElement;

  const rectDimension: number = remapDataSetValueToSvgDimension(d.value);
  const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;

  rect.setAttribute('width', `${rectDimension}`);
  // ...setting the rest of the attributes...

  mask.setAttribute('id', `maskW${rectDimension.toFixed()}`);

  mask.appendChild(rect);

  // ...creating and setting the attributes for the smaller rectangle...

  svg.appendChild(mask);
});

data.forEach((d: DataSetEntry, index: number) => {
    // ...our code to generate the colored rectangles...
});

Podríamos usar el índice como ID de la máscara, pero esta parece una opción más legible, al menos para mí:

mask.setAttribute('id', `maskW${rectDimension.toFixed()}`); // maskW320, masW240, ...

En cuanto a agregar el rectángulo más pequeño en la máscara, tenemos fácil acceso al valor que necesitamos porque previamente ordenamos los valores del rectángulo de mayor a menor. Eso significa que el siguiente elemento en el bucle es el rectángulo más pequeño, al que debemos hacer referencia. Y podemos hacerlo por su índice.

// ...previous part where we created the mask and the rectangle...

const smallerRectIndex = index + 1;

// there's no next one when we are on the smallest
if (data[smallerRectIndex] !== undefined) {
  const smallerRectDimension: number = remapDataSetValueToSvgDimension(
    data[smallerRectIndex].value
  );
  const smallerRect: SVGRectElement = createSvgNSElement(
    'rect'
  ) as SVGRectElement;

  // ...setting the rectangle attributes...

  mask.appendChild(smallerRect);
}

svg.appendChild(mask);

Lo que queda es agregar el mask atributo al rectángulo de color en nuestro bucle original. Debe coincidir con el formato que elegimos:

rect.setAttribute('mask', `url(#maskW${rectDimension.toFixed()})`); // maskW320, maskW240, ...

El resultado final

¡Y hemos terminado! Hemos creado con éxito un gráfico que está hecho de cuadrados anidados. Incluso se deshace al pasar el mouse por encima. Y todo lo que necesitó fue algo de SVG usando el <mask> para dibujar el área recortada de cada cuadrado.

(Visited 4 times, 1 visits today)