Capítulo 4. Eventos, interactividad y animación

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Cuando los renderiza un navegador, los elementos SVG pueden recibir eventos del usuario y pueden manipularse como un todo (por ejemplo, para cambiar su posición o apariencia). Esto significa que se comportan esencialmente como widgets en un conjunto de herramientas GUI. Es una propuesta apasionante: considerar SVG como un conjunto de widgets para gráficos. Este capítulo trata de las opciones disponibles para crear esas características esenciales de una interfaz de usuario: interactividad y animación.

Eventos

Un aspecto importante del DOM es su modelo de eventos: básicamente, cualquier elemento del DOM puede recibir eventos e invocar un controlador apropiado. El número de tipos de eventos diferentes es muy grande; los más importantes para nuestros propósitos son los eventos generados por el usuario (clics o movimientos del ratón, así como pulsaciones de teclas; véase la Tabla 4-1).1

Tabla 4-1. Algunos tipos importantes de eventos generados por el usuario
Función Descripción

click

Se pulsa y suelta cualquier botón del ratón sobre un elemento.

mousemove

El ratón se mueve mientras está sobre un elemento.

mousedown, mouseup

Se pulsa o suelta un botón del ratón sobre un elemento.

mouseenter, mouseleave

El puntero del ratón se mueve sobre un elemento o fuera de él.

mouseover, mouseout

El puntero del ratón se mueve sobre o fuera de un elemento, o de cualquiera de sus hijos.

keydown, keyup

Se pulsa o suelta cualquier tecla.

D3 trata el manejo de eventos como parte de la abstracción Selection (verTabla 4-2). Si sel es una instancia de Selection, utiliza la siguiente función miembro para registrar una llamada de retorno como manejador de eventos para el tipo de evento especificado:

sel.on( type, callback )

El argumento type debe ser una cadena que indique el tipo de evento (como"click"). Se permite cualquier tipo de evento DOM. Si ya se había registrado un controlador para el tipo de evento a través de on(), se eliminará antes de registrar el nuevo controlador. Para eliminar explícitamente el controlador para un tipo de evento determinado, proporciona null como segundo argumento. Para registrar varios controladores para el mismo tipo de evento, el nombre del tipo puede ir seguido de un punto y una etiqueta arbitraria (de modo que el controlador para "click.foo" no sobrescriba al de "click.bar").

La llamada de retorno es una función que se invoca cuando cualquier elemento de la selección recibe un evento del tipo especificado. La llamada de retorno se invocará de la misma forma que se invoca a cualquier otro accesor en el contexto de una selección, pasándole el punto de datos d vinculado al elemento actual, el índice del elemento i en la selección actual, y los nodos de la selección actual, mientras que this contiene el propio elemento actual.2 La instancia real del evento no se pasa a la llamada de retorno como argumento, pero está disponible en la variable:

d3.event

Cuando se produce un evento, esta variable contiene la instancia en bruto del evento DOM (¡no una envoltura D3!). La información proporcionada por el propio objeto de evento depende del tipo de evento. Para los eventos de ratón, naturalmente es de especial interés laubicación del puntero del ratón cuando se produjo el evento. El objeto evento contiene las coordenadas del ratón con respecto a tres sistemas de coordenadas diferentes,3 pero ninguna de ellas proporciona directamente la información que sería más útil, es decir, ¡la posición con respecto al elemento padre contenedor! Afortunadamente, pueden obtenerse utilizando:

((("d3.mouse() function")))d3.mouse( node )

Esta función devuelve las coordenadas del ratón como una matriz de dos elementos [x, y]. El argumento debe ser el elemento contenedor que lo rodea (como un DOM Node, no como Selection). Cuando trabajes con SVG, puedes proporcionar cualquier elemento (como Node), y la función calculará las coordenadas relativas al elemento SVG antepasado más cercano.

Tabla 4-2. Algunos métodos, variables y funciones importantes relacionados con la gestión de eventos (sel es un objeto Selección)
Función Descripción

sel.on( types, callback )

Añade o elimina una llamada de retorno para cada elemento de la selección. El argumento typesdebe ser una cadena formada por uno o más nombres de tipo de evento, separados por espacios en blanco. Un tipo de evento puede ir seguido de un punto y una etiqueta arbitraria para permitir que se registren varios manejadores para un mismo tipo de evento.

  • Si se especifica una llamada de retorno, se registra como manejador del evento; primero se elimina cualquier manejador de eventos existente.

  • Si el argumento de la llamada de retorno es null, se elimina cualquier manejador existente.

  • Si falta el argumento de la llamada de retorno, se devuelve el manejador asignado actualmente.

d3.event

Contiene el evento actual, si lo hay, como un objeto DOM Event.

d3.mouse( parent )

Devuelve una matriz de dos elementos que contiene las coordenadas del ratón relativas al padre especificado.

sel.dispatch( type )

Envía un evento personalizado del tipo especificado a todos los elementos de la selección actual.

Explorar gráficos con el ratón

Para alguien que trabaje analíticamente con datos, estas funciones ofrecen algunas oportunidades interesantes, porque facilitan la exploración interactiva de los gráficos: señala con el ratón un punto del gráfico y obtén información adicional sobre el punto de datos situado allí. He aquí un ejemplo sencillo. Si llamas a la función del Ejemplo 4-1, mientras proporcionas una cadena de selectores CSS (ver "Selectores CSS") que identifique un elemento <svg>, la ubicación actual del puntero del ratón (en coordenadas de píxeles) se mostrará en el propio gráfico. Además, la ubicación de la visualización textual no es fija, sino que se moverá junto con el puntero del ratón.

Ejemplo 4-1. Dada una cadena de selectores CSS, esta función mostrará continuamente la posición del ratón en coordenadas de píxeles cada vez que el usuario mueva el ratón.
function coordsPixels( selector ) {
    var txt = d3.select( selector ).append( "text" );             1
    var svg = d3.select( selector ).attr( "cursor", "crosshair" ) 2
        .on( "mousemove", function() {
            var pt = d3.mouse( svg.node() );                      3
            txt.attr( "x", 18+pt[0] ).attr( "y", 6+pt[1] )        4
                .text( "" + pt[0] + "," + pt[1] );
        } );
}
1

Crea el elemento <text> para mostrar las coordenadas. Es importante hacerlo fuera de la llamada de retorno del evento; de lo contrario, ¡se creará un nuevo elemento <text>cada vez que el usuario mueva el ratón!

2

Cambia la forma del cursor del ratón mientras está sobre el elemento <svg>. Esto no es necesario, por supuesto, pero es un efecto adecuado (y también demuestra cómo se puede cambiar el cursor del ratón mediante atributos; véase el Apéndice B).

3

Obtén las coordenadas del ratón, relativas a la esquina superior izquierda del elemento <svg>, utilizando la función de conveniencia d3.mouse().

4

Actualiza el elemento de texto creado anteriormente. En este ejemplo, se actualizan tanto el contenido de texto mostrado del elemento como su posición: ligeramente a la derecha de la posición del ratón.

Mostrar las coordenadas del ratón no es, por supuesto, ni nuevo ni especialmente emocionante. Pero lo emocionante es ver lo fácil que es implementar ese comportamiento en D3.

Caso práctico: Resaltado simultáneo

El siguiente ejemplo es más interesante. Aborda un problema habitual cuando se trabaja con conjuntos de datos multivariantes: cómo enlazar visualmente dos vistas o proyecciones diferentes de los datos. Una forma consiste en seleccionar con el ratón una región de puntos de datos en una vista y resaltar simultáneamente los puntos correspondientes en las demás vistas. En la Figura 4-1, los puntos se resaltan en ambos paneles según su distancia (en coordenadas de píxeles) desde el puntero del ratón en el panel izquierdo. Como este ejemplo es más complicado, primero discutiremos una versión simplificada (ver Ejemplo 4-2).

dfti 0401
Figura 4-1. Los puntos de datos pertenecientes al mismo registro se resaltan simultáneamente en ambos paneles, en función de la distancia de los puntos del panel izquierdo al puntero del ratón.
Ejemplo 4-2. Comandos de la Figura 4-1
function makeBrush() {
    d3.csv( "dense.csv" ).then( function( data ) {                1
        var svg1 = d3.select( "#brush1" );                        2
        var svg2 = d3.select( "#brush2" );

        var sc1=d3.scaleLinear().domain([0,10,50])                3
            .range(["lime","yellow","red"]);
        var sc2=d3.scaleLinear().domain([0,10,50])
            .range(["lime","yellow","blue"]);

        var cs1 = drawCircles(svg1,data,d=>d["A"],d=>d["B"],sc1); 4
        var cs2 = drawCircles(svg2,data,d=>d["A"],d=>d["C"],sc2);

        svg1.call( installHandlers, data, cs1, cs2, sc1, sc2 );   5
    } );
}

function drawCircles( svg, data, accX, accY, sc ) {
    var color = sc(Infinity);                                     6
    return svg.selectAll( "circle" ).data( data ).enter()
        .append( "circle" )
        .attr( "r", 5 ).attr( "cx", accX ).attr( "cy", accY )
        .attr( "fill", color ).attr( "fill-opacity", 0.4 );
}

function installHandlers( svg, data, cs1, cs2, sc1, sc2 ) {
    svg.attr( "cursor", "crosshair" )
        .on( "mousemove", function() {
            var pt = d3.mouse( svg.node() );

            cs1.attr( "fill", function( d, i ) {                  7
                var dx = pt[0] - d3.select( this ).attr( "cx" );
                var dy = pt[1] - d3.select( this ).attr( "cy" );
                var r = Math.hypot( dx, dy );

                data[i]["r"] = r;                                 8
                return sc1(r); } );                               9

            cs2.attr( "fill", (d,i) => sc2( data[i]["r"] ) ); } ) 10

        .on( "mouseleave", function() {
            cs1.attr( "fill", sc1(Infinity) );                    11
            cs2.attr( "fill", sc2(Infinity) ); } );
}
1

Carga el conjunto de datos y especifica la llamada de retorno a invocar cuando los datos estén disponibles (consulta el Capítulo 6 para obtener más información sobre la obtención de datos). El archivo contiene tres columnas, etiquetadas como A, B, y C.

2

Selecciona los dos paneles del gráfico.

3

D3 puede interpolar suavemente entre colores. Aquí creamos dos gradientes de color (uno para cada panel). (Consulta el Capítulo 7 para saber más sobre interpolación y objetos de escala).

4

Crea los círculos que representan puntos de datos. Los círculos recién creados se devuelven como objetos Selection. Siguiendo una convención general de D3, las columnas se especifican en la llamada a la función proporcionando funciones accesorias.

5

Llama a la función installHandlers() para registrar los controladores de eventos. Esta línea de código utiliza la función call() para invocar a la funcióninstallHandlers(), al tiempo que proporciona la selección svg1 y el resto de parámetros como argumentos. (Ya nos encontramos con esto en el Ejemplo 2-6; véase también la discusión relativa a los componentes en el Capítulo 5.)

6

Inicialmente, los círculos se dibujan con el color "máximo". Para encontrar este color, evalúa la escala de colores en el infinito positivo.

7

Para cada punto del panel de la izquierda, calcula su distancia al puntero del ratón...

8

... y almacenarlo, como columna adicional, en el conjunto de datos. (Éste será nuestro mecanismo de comunicación entre los dos paneles de la figura).

9

Devuelve el color apropiado del gradiente de color.

10

Utiliza la columna adicional del conjunto de datos para colorear los puntos del panel de la derecha.

11

Restaura los puntos a sus colores originales cuando el ratón abandone el panel izquierdo.

Esta versión del programa funciona bien y resuelve el problema original. La versión mejorada de la función installHandlers() mostrada enel Ejemplo 4-3 nos permite discutir algunas técnicas adicionales a la hora de escribir este tipo de código de interfaz de usuario.

Ejemplo 4-3. Una versión mejorada de la función installHandlers() del Ejemplo 4-2
function installHandlers2( svg, data, cs1, cs2, sc1, sc2 ) {
    var cursor = svg.append( "circle" ).attr( "r", 50 )           1
        .attr( "fill", "none" ).attr( "stroke", "black" )
        .attr( "stroke-width", 10 ).attr( "stroke-opacity", 0.1 )
        .attr( "visibility", "hidden" );                          2

    var hotzone = svg.append( "rect" ).attr( "cursor", "none" )   3
        .attr( "x", 50 ).attr( "y", 50 )
        .attr( "width", 200 ).attr( "height", 200 )
        .attr( "visibility", "hidden" )                           4
        .attr( "pointer-events", "all" )

        .on( "mouseenter", function() {                           5
            cursor.attr( "visibility", "visible" ); } )

        .on( "mousemove", function() {                            6
            var pt = d3.mouse( svg.node() );
            cursor.attr( "cx", pt[0] ).attr( "cy", pt[1] );

            cs1.attr( "fill", function( d, i ) {
                var dx = pt[0] - d3.select( this ).attr( "cx" );
                var dy = pt[1] - d3.select( this ).attr( "cy" );
                var r = Math.hypot( dx, dy );

                data[i]["r"] = r;
                return sc1(r); } );

            cs2.attr( "fill", (d,i) => sc2( data[i]["r"] ) ); } )

        .on( "mouseleave", function() {
            cursor.attr( "visibility", "hidden" );
            cs1.attr( "fill", sc1(Infinity) );
            cs2.attr( "fill", sc2(Infinity) ); } )
}
1

En esta versión, el propio puntero del ratón se oculta y se sustituye por un círculo grande y parcialmente opaco. Los puntos dentro del círculo se resaltarán.

2

Inicialmente, el círculo está oculto. Sólo se mostrará cuando el puntero del ratón entre en la "zona caliente".

3

La "zona caliente" se define como un rectángulo dentro del panel izquierdo. Los manejadores de eventos se registran en este rectángulo, lo que significa que sólo se invocarán cuando el puntero del ratón esté dentro de él.

4

El rectángulo queda oculto a la vista. Por defecto, los elementos DOM que tienen su atributo visibility fijado en hidden no reciben eventos del puntero del ratón. Para evitarlo, el atributo pointer-eventsdebe establecerse explícitamente. (Otra forma de hacer invisible un elemento es establecer su fill-opacity a 0. En este caso, no será necesario modificar el atributo pointer-events ).

5

Cuando el ratón entra en la "zona caliente", se muestra el círculo opaco que actúa como puntero.

6

Los manejadores de eventos mousemove y mouseleave son equivalentes a los del Ejemplo 4-2, excepto por los comandos adicionales para actualizar el círculo que actúa como cursor.

El uso de una "zona caliente" activa en este ejemplo es, por supuesto, opcional, pero demuestra una técnica interesante. Al mismo tiempo, la discusión sobre el atributo pointer-events sugiere que este tipo de programación de la interfaz de usuario puede implicar retos inesperados. Volveremos sobre este punto después del siguiente ejemplo.

El componente de comportamiento de arrastrar y soltar D3

Varios patrones comunes de la interfaz de usuario consisten en una combinación de eventos y respuestas: en el patrón de arrastrar y soltar, por ejemplo, el usuario primero selecciona un elemento, luego lo mueve y, por último, lo suelta de nuevo. D3 incluye una serie de componentes de comportamiento predefinidos que simplifican el desarrollo de dicho código de interfaz de usuario al agrupar y organizar las acciones necesarias. Además, estos componentes también unifican algunos detalles de la interfaz de usuario.

Considera una situación como la de la Figura 4-2, que muestra el siguiente fragmento SVG:

<svg id="dragdrop" width="600" height="200">
  <circle cx="100" cy="100" r="20" fill="red" />
  <circle cx="300" cy="100" r="20" fill="green" />
  <circle cx="500" cy="100" r="20" fill="blue" />
</svg>
dfti 0402
Figura 4-2. Configuración inicial del comportamiento arrastrar y soltar

Ahora vamos a permitir que el usuario cambie la posición de los círculos con el ratón. No es difícil añadir el conocido patrón de arrastrar y soltar registrando llamadas de retorno para los eventos mousedown, mousemove y mouseup, pero el Ejemplo 4-4 utiliza en su lugar el componente de comportamiento D3 drag. Como se explica en el Ejemplo 2-6, un componente es un objeto de función que toma como argumento una instancia de Selection y añade elementos DOM a esa Selection (consulta también el Capítulo 5). Un componente de comportamiento es un componente que instala las retrollamadas de eventos necesarias en el árbol DOM. Al mismo tiempo, también es un objeto que tiene funciones miembro en sí mismo. El listado utiliza la función miembro on( type, callback ) del componente de arrastre para especificar las retrollamadas de los distintos tipos de eventos.

Ejemplo 4-4. Utilizar el comportamiento arrastrar y soltar
function makeDragDrop() {
    var widget = undefined, color = undefined;

    var drag = d3.drag()                                          1
        .on( "start", function() {                                2
            color = d3.select( this ).attr( "fill" );
            widget = d3.select( this ).attr( "fill", "lime" );
        } )
        .on( "drag", function() {                                 3
            var pt = d3.mouse( d3.select( this ).node() );
            widget.attr( "cx", pt[0] ).attr( "cy", pt[1] );
        } )
        .on( "end", function() {                                  4
            widget.attr( "fill", color );
            widget = undefined;
        } );

    drag( d3.select( "#dragdrop" ).selectAll( "circle" ) );       5
}
1

Crea un objeto función drag utilizando la función de fábrica d3.drag(), luego invoca la función miembro on() en el objeto función devuelto para registrar las llamadas de retorno necesarias.

2

El manipulador start almacena el color actual del círculo seleccionado; a continuación, cambia el color del círculo seleccionado y asigna el propio círculo seleccionado (como Selection) a widget.

3

El manejador drag recupera las coordenadas actuales del ratón y mueve el círculo seleccionado a esta ubicación.

4

El manejador end restaura el color del círculo y borra elwidget activo.

5

Por último, invoca la operación del componente drag proporcionando una selección que contenga los círculos para instalar los manejadores de eventos configurados en la selección.

Una forma más idiomática de expresarlo sería utilizar la función call() en lugar de invocar explícitamente la operación componente:

d3.select( "#dragdrop" ).selectAll( "circle" )
    .call( d3.drag()
           .on( "start", function() { ... } )
           .on( "drag", function() { ... } )
           .on( "end", function() { ... } ) );

Los nombres de los eventos del Ejemplo 4-4 pueden sorprenderte: no se trata de eventos DOM estándar, sino de pseudoeventos D3. El comportamiento D3 drag combina la gestión de eventos del ratón y de la pantalla táctil. Internamente, el pseudoevento start corresponde a un evento mousedowno touchstart, y algo similar para drag y end. Además, el comportamiento drag evita la acción por defecto del navegador para ciertos tipos de eventos.4 D3 incluye comportamientos adicionales para ayudar con el zoom y al seleccionar partes de un gráfico con el ratón.

Notas sobre la programación de la interfaz de usuario

Espero que los ejemplos hasta ahora te hayan convencido de que crear gráficos interactivos utilizando D3 no tiene por qué ser difícil; de hecho, creo que D3 los hace viables incluso para tareas y exploraciones ad hoc y puntuales. Al mismo tiempo, como muestra la discusión tras los dos ejemplos anteriores, la programación de interfaces gráficas de usuario sigue siendo un problema relativamente complejo. Participan muchos componentes, cada uno con sus propias reglas, y pueden interactuar de formas inesperadas. Los navegadores pueden diferir en su implementación. He aquí algunos recordatorios y posibles sorpresas (consulta también el Apéndice C para obtener información de fondo sobre la gestión de eventos DOM):

  • Las llamadas repetidas a on() para el mismo tipo de evento en la misma instancia Selectionse emborronan entre sí. Añade una etiqueta única al tipo de evento (separada por un punto) para registrar varios manejadores de eventos.

  • Si quieres acceder a this en una función de devolución de llamada o accesoria, debesutilizar la palabra clave function, no puedes utilizar una función de flecha. Se trata de una limitación del lenguaje JavaScript (véase el Apéndice C). Puedes encontrar ejemplos en la función installHandlers() de los Ejemplos 4-2 y 4-3, y varias veces en elEjemplo 4-4.

  • El comportamiento por defecto del navegador puede interferir con tu código; puede que tengas que impedirlo explícitamente.

  • Generalmente, sólo los elementos visibles y pintados pueden recibir eventos de puntero del ratón. Los elementos con el atributo visibility establecido en hidden, o con fill y stroke establecidos en none, no reciben eventos de puntero por defecto. Utiliza y el atributo pointer-events para un control más preciso de las condiciones en las que los elementos recibirán eventos (consulta MDN Eventos de puntero).

  • Con un espíritu similar, un elemento <g> no tiene representación visual y, por tanto, no genera eventos de puntero. No obstante, puede ser conveniente registrar un controlador de eventos en un elemento <g> porque los eventos generados por cualquiera de sus hijos (visibles) se delegarán en él. (Utiliza un rectángulo invisible u otra forma para definir "zonas calientes" activas, como en Ejemplo 4-3.)

Transiciones suaves

Una forma obvia de responder a los acontecimientos es aplicar algún cambio a la apariencia o configuración de la figura (por ejemplo, para mostrar un efecto de antes y después). En este caso, suele ser útil dejar que el cambio se produzca gradualmente, en lugar de instantáneamente, para llamar la atención sobre el cambio que se está produciendo y permitir a los usuarios discernir detalles adicionales. Por ejemplo, ahora los usuarios pueden ser capaces de reconocer qué puntos de datos se ven más afectados por el cambio, y cómo (consulta la Figura 3-3 y el Ejemplo 3-1 para ver un ejemplo).

Convenientemente, la herramienta D3 Transition hace todo el trabajo por ti. Reproduce la mayor parte de la API Selection, y puedes cambiar la apariencia de los elementos seleccionados utilizando attr() o style() como antes (ver Capítulo 3). Pero ahora los nuevos ajustes no surten efecto inmediatamente, sino que se aplican gradualmente a lo largo de un periodo de tiempo configurable (consulta el Ejemplo 2-8 para ver un primer ejemplo).

Bajo cuerda, D3 crea y programa las configuraciones intermedias necesarias para dar la apariencia de que el gráfico cambia suavemente a lo largo de la duración deseada. Para ello, D3 invoca uninterpoladorque crea las configuraciones intermedias entre los puntos inicial y final. La función de interpolación de D3 es bastante inteligente y capaz de interpolar automáticamente entre la mayoría de los tipos (como números, fechas, colores, cadenas con números incrustados, etc.; consulta el Capítulo 7 para obtener una descripción detallada).

Crear y configurar transiciones

El flujo de trabajo para crear una transición es sencillo (consulta tambiénla Tabla 4-3):

  1. Antes de crear una transición, asegúrate de que se han enlazado todos los datos y de que se han creado todos los elementos que deben formar parte de la transición (utilizando append() o insert())-¡incluso si inicialmente están configurados para ser invisibles! (La API Transition te permite cambiar y eliminar elementos, pero no prevé la creación de elementos como parte de la transición).

  2. Ahora selecciona los elementos que deseas cambiar utilizando la conocida API Selection.

  3. Invoca a transition() sobre esta selección para crear una transición. Opcionalmente, llama a duration(), delay(), o ease() para tener más control sobre su comportamiento.

  4. Establece el estado final deseado utilizando attr() o style() como de costumbre. D3 creará las configuraciones intermedias entre los valores actuales y los estados finales indicados, y las aplicará durante la duración de la transición.

A menudo, estos comandos formarán parte de un controlador de eventos, de modo que la transición se inicie cuando se produzca un evento apropiado.

Tabla 4-3. Funciones para crear y terminar una transición (sel es un objeto Selección; trans es un objetoTransición)
Función Descripción

sel.transition( tag )

Devuelve una nueva transición en la selección receptora. El argumento opcional puede ser una cadena (para identificar y distinguir esta transición en la selección) o una instancia de Transition (para sincronizar las transiciones).

sel.interrupt( tag )

Detiene la transición activa y cancela cualquier transición pendiente en los elementos seleccionados para el identificador dado. (Las interrupciones no se reenvían a los hijos de los elementos seleccionados).

trans.transition()

Devuelve una nueva transición sobre los mismos elementos seleccionados que la transición receptora, programada para iniciarse cuando finalice la transición actual. La nueva transición hereda la configuración de la transición actual.

trans.selection()

Devuelve la selección de una transición.

Además del punto final deseado, un Transition también te permite configurar varios aspectos de su comportamiento (ver Tabla 4-4). Todos ellos tienen valores predeterminados razonables, por lo que la configuración explícita es opcional:

  • Un retraso que debe transcurrir antes de que el cambio empiece a surtir efecto.

  • Una duración durante la cual el ajuste cambiará gradualmente.

  • Una atenuación que controla cómo variará la velocidad de cambio a lo largo de la duración de la transición (para "entrar" y "salir" de la animación). Por defecto, la atenuación sigue un polinomio cúbico a trozos con un comportamiento de "entrada lenta, salida lenta".

  • Un interpolador para calcular los valores intermedios (rara vez es necesario, porque los interpoladores por defecto manejan automáticamente las configuraciones más habituales).

  • Un controlador de eventos para ejecutar código personalizado cuando la transición comience, termine o se interrumpa.

Tabla 4-4. Funciones para configurar una transición o para recuperar la configuración actual si se llama sin argumento (trans es un objeto Transición)
Función Descripción

trans.delay( value )

Establece el retardo (en milisegundos) antes de que comience la transición para cada elemento de la selección; el valor por defecto es 0. El retardo puede darse como una constante o como una función. Si es una función, la función se invocará una vez por cada elemento, antes de que comience la transición, y debe devolver el retardo deseado. A la función se le pasarán los datos vinculados al elemento d y su índice en la selección i.

trans.duration( value )

Establece la duración (en milisegundos) de la transición para cada elemento de la selección; el valor por defecto es 250 milisegundos. La duración puede darse como una constante o como una función. Si es una función, la función se invocará una vez para cada elemento, antes de que comience la transición, y debe devolver la duración deseada. A la función se le pasarán los datos vinculados al elemento d y su índice en la selección i.

trans.ease( fct )

Establece la función de servidumbre para todos los elementos seleccionados. La servidumbre debe ser una función, que tome un único parámetro entre 0 y 1, y devuelva un único valor, también entre 0 y 1. La servidumbre por defecto es d3.easeCubic(un polinomio cúbico definido a trozos con un comportamiento de "entrada lenta, salida lenta").

trans.on( type, handler )

Añade un controlador de eventos a la transición. El tipo debe ser start, end, o interrupt. El controlador de eventos se invocará en el punto apropiado del ciclo de vida de la transición. Esta función se comporta de forma similar a la función on()en un objeto Selection (ver Tabla 4-2).

Utilizar transiciones

La API Transition reproduce gran parte de la API Selection. En concreto, están disponibles todas las funciones de la Tabla 3-2 (es decir,select(), selectAll() y filter()). De laTabla 3-4, se mantienen attr(), style(), text(), y each(), así como todas las funciones de la Tabla 3-5 exceptoappend(), insert(), y sort(). (Como se ha señalado antes, todos los elementos que participan en una transición deben existir antes de que se cree la transición. Por la misma razón, ninguna de las funciones para vincular datos de la Tabla 3-3 existe para las transiciones).

Las transiciones básicas son fáciles de utilizar, como ya hemos visto en un ejemplo de un capítulo anterior(Ejemplo 3-1). La aplicación del Ejemplo 4-5 sigue siendo sencilla, pero el efecto es más sofisticado: un gráfico de barras se actualiza con nuevos datos, pero el efecto seescalona (utilizando delay()) para que las barras no cambien todas al mismo tiempo.

dfti 0403
Figura 4-3. Cuando este gráfico de barras se actualiza con un nuevo conjunto de datos, las actualizaciones se aplican consecutivamente, de izquierda a derecha.
Ejemplo 4-5. Utilizar transiciones (ver Figura 4-3)
function makeStagger() {
    var ds1 = [ 2, 1, 3, 5, 7, 8, 9, 9, 9, 8, 7, 5, 3, 1, 2 ];    1
    var ds2 = [ 8, 9, 8, 7, 5, 3, 2, 1, 2, 3, 5, 7, 8, 9, 8 ];
    var n = ds1.length, mx = d3.max( d3.merge( [ds1, ds2] ) );    2

    var svg = d3.select( "#stagger" );

    var scX = d3.scaleLinear().domain( [0,n] ).range( [50,540] ); 3
    var scY = d3.scaleLinear().domain( [0,mx] ).range( [250,50] );

    svg.selectAll( "line" ).data( ds1 ).enter().append( "line" )  4
        .attr( "stroke", "red" ).attr( "stroke-width", 20 )
        .attr( "x1", (d,i)=>scX(i) ).attr( "y1", scY(0) )
        .attr( "x2", (d,i)=>scX(i) ).attr( "y2", d=>scY(d) );

    svg.on( "click", function() {                                 5
        [ ds1, ds2 ] = [ ds2, ds1 ];                              6

        svg.selectAll( "line" ).data( ds1 )                       7
            .transition().duration( 1000 ).delay( (d,i)=>200*i )  8
            .attr( "y2", d=>scY(d) );                             9
    } );
}
1

Define dos conjuntos de datos. Para simplificar las cosas, sólo se incluyen los valores y; utilizaremos el índice de matriz de cada elemento para su posición horizontal.

2

Halla el número de puntos de datos y el valor máximo global de ambos conjuntos de datos.

3

Dos objetos de escala que asignan los valores del conjunto de datos a coordenadas verticales, y sus posiciones de índice en la matriz a coordenadas de píxel horizontales.

4

Crea el gráfico de barras. Cada "barra" se realiza como una línea gruesa (en lugar de un elemento <rect> ).

5

Registra un controlador de eventos para los eventos de "click".

6

Intercambia los conjuntos de datos.

7

Vincula el conjunto de datos (actualizado) ds1 a la selección...

8

... y crea una instancia de transición. Cada barra tardará un segundo en alcanzar su nuevo tamaño, pero sólo se iniciará tras un retardo. El retraso depende de la posición horizontal de cada barra, creciendo de izquierda a derecha. Esto tiene el efecto de que la "actualización" parece barrer todo el gráfico.

9

Por último, fija la nueva longitud vertical de cada línea. Éste es el punto final de la transición.

Consejos y técnicas

No todas las transiciones son tan sencillas como las que hemos visto hasta ahora. He aquí algunos consejos y técnicas adicionales.

Cuerdas

Los interpoladores predeterminados de D3 interpolarán los números que estén incrustados en cadenas, pero dejarán solo el resto de la cadena, porque no hay ninguna forma útil de interpolar entre cadenas. La mejor forma de conseguir una transición suave entre cadenas es hacer un fundido cruzado entredos cadenas en el mismo lugar. Supongamos que existen dos elementos <text>adecuados:

<text id="t1" x="100" y="100" fill-opacity="1">Hello</text>
<text id="t2" x="100" y="100" fill-opacity="0">World</text>

Luego puedes hacer fundidos cruzados entre ellos cambiando su opacidad (posiblemente cambiando la duración de la transición):

d3.select("#t1").transition().attr( "fill-opacity", 0 );
d3.select("#t2").transition().attr( "fill-opacity", 1 );

Una alternativa que puede tener sentido en ciertos casos es escribir un interpolador personalizado para generar valores de cadena intermedios.

Transiciones encadenadas

Las transiciones pueden encadenarse de modo que una transición comience cuando termine la primera. Las transiciones posteriores heredan la duración y el retardo de la transición anterior (a menos que se anulen explícitamente). El código siguiente hará que los elementos seleccionados pasen primero a rojo y luego a azul:

d3.selectAll( "circle" )
    .transition().duration( 2000 ).attr( "fill", "red" )
    .transition().attr( "fill", "blue" );

Configuración inicial explícita

A menos que pienses utilizar un interpolador personalizado (ver a continuación), es importante que la configuración inicial se establezca explícitamente. Por ejemplo, no confíes en el valor por defecto (black) para el atributo fill: a menos que el atributo de relleno se establezca explícitamente, el interpolador por defecto no sabrá qué hacer.

Interpoladores personalizados

Utilizando los métodos de la Tabla 4-5, es posible especificar un interpolador personalizado que se utilizará durante la transición. Los métodos para establecer un interpolador personalizado toman como argumento una función de fábrica. Cuando se inicia la transición, se invoca a la función de fábrica para cada elemento de la selección, pasándole los datos d vinculados al elemento y el índice del elemento i, con this establecido en el DOM actualNode. La fábrica debe devolver una función interpoladora. La función interpoladora debe aceptar un único argumento numérico entre 0 y 1 y debe devolver un valor intermedio adecuado entre la configuración inicial y la final. Se llamará al interpolador después de aplicar cualquier suavizado. El código siguiente utiliza un interpolador de color personalizado sencillo sin suavizado (consulta el Capítulo 8 para conocer formas más flexibles de operar con los colores en D3):

d3.select( "#custom" ).selectAll( "circle" )
    .attr( "fill", "white" )
    .transition().duration( 2000 ).ease( t=>t )
    .attrTween( "fill", function() {
        return t => "hsl(" + 360*t + ", 100%, 50%)"
    } );

El siguiente ejemplo es más interesante. Crea un rectángulo centrado en la posición (100, 100) del gráfico y luego rota el rectángulo suavemente alrededor de su centro. (Los interpoladores por defecto de D3 comprenden algunas transformaciones SVG, pero este ejemplo demuestra cómo escribir tu propio interpolador en caso de que lo necesites).

d3.select( "#custom" ).append( "rect" )
    .attr( "x", 80 ).attr( "y", 80 )
    .attr( "width", 40 ).attr( "height", 40 )
    .transition().duration( 2000 ).ease( t=>t )
    .attrTween( "transform", function() {
        return t => "rotate(" + 360*t + ",100,100)"
    } );

Actos de transición

Las transiciones emiten eventos personalizados cuando comienzan, terminan y se interrumpen. Utilizando el método on(), puedes registrar un manejador de eventos en una transición, que será llamado cuando se emita el evento del ciclo de vida apropiado. (Consulta la Documentación de Referencia D3 para más detalles).

Servidumbres

Utilizando el método ease(), puedes especificar una servidumbre. El propósito de una servidumbre es "estirar" o "comprimir" el tiempo visto por el interpolador, permitiendo que la animación "entre y salga" del movimiento. Esto suele mejorar mucho el efecto visual de una animación . De hecho, los animadores de Disney han reconocido que "entrar y salir despacio" es uno de los "Doce Principios de la Animación" (ver "Principios de la Animación"). Pero otras veces, cuando no se ajustan bien al modo en que el usuario espera que se comporte un objeto, los aligeramientos pueden resultar francamente confusos. El equilibrio es definitivamente sutil.

Una servidumbre toma un parámetro en el intervalo [0, 1] y lo mapea al mismo intervalo, comenzando para t = 0 en 0 y terminando para t = 1 en 1. El mapeo suele ser no lineal (de lo contrario, la servidumbre es simplemente la identidad). La flexión por defecto es d3.easeCubic, que implementa una versión del comportamiento "entrar despacio, salir despacio".

Técnicamente, una servidumbre es simplemente un mapeado que se aplica al parámetro temporal t antes de pasarlo al interpolador. Esto hace que la distinción entre la servidumbre y el interpolador sea un tanto arbitraria. ¿Qué ocurre si un interpolador personalizado manipula el parámetro de tiempo de alguna forma no lineal? Desde un punto de vista práctico, parece mejor tratar las servidumbres como una función práctica que añade un comportamiento de "entrada lenta, salida lenta" a los interpoladores estándar. (D3 incluye una gama confusamente amplia de easings diferentes, algunos de los cuales difuminan considerablemente la distinción entre un easing y lo que debería considerarse un interpolador personalizado).

No abuses de las transiciones

Las transiciones pueden utilizarse en exceso. Un problema general de las transiciones es que normalmente no pueden ser interrumpidas por el usuario: la espera forzada resultante puede conducir rápidamente a la frustración. Cuando las transiciones se emplean para que el usuario pueda seguir los efectos de un cambio, ayudan a la comprensión (véase un ejemplo sencillo en la Figura 3-3 ). Pero cuando se utilizan sólo "por efecto", cansan fácilmente una vez que desaparece la simpatía inicial.(La Figura 4-3 puede servir de ejemplo de advertencia en este sentido).

Tabla 4-5. Métodos para especificar interpoladores personalizados (trans es un objeto Transición)
Función Descripción

trans.attrTween( name, factory )

Establece un interpolador personalizado para el atributo nombrado. El segundo argumento debe ser un método de fábrica que devuelva un interpolador.

trans.styleTween( name, factory )

Establece un interpolador personalizado para el estilo nombrado. El segundo argumento debe ser un método de fábrica que devuelva un interpolador.

trans.tween( tag, factory )

Establece un interpolador personalizado que se invocará durante las transiciones. El primer argumento es una etiqueta arbitraria para identificar este interpolador, el segundo argumento debe ser un método de fábrica que devuelva un interpolador. El efecto del interpolador no está restringido; se invocará únicamente por sus efectos secundarios.

Animación con Eventos Temporizados

Las transiciones son una técnica cómoda para transformar suavemente una configuración en otra, pero no están pensadas como marco para animaciones generales. Para crearlas, suele ser necesario trabajar en un nivel inferior. D3 incluye un temporizador especial que invocará una llamada de retorno determinada una vez por fotograma de animación, es decir, cada vez que el navegador esté a punto de repintar la pantalla. El intervalo de tiempo no es configurable porque viene determinado por la frecuencia de actualización del navegador (unas 60 veces por segundo o cada 17 milisegundos, en la mayoría de los navegadores). Tampoco es exacto; a la llamada de retorno se le pasará una marca de tiempo de alta precisión que puede utilizarse para determinar cuánto tiempo ha pasado desde la última invocación (ver Tabla 4-6).

Tabla 4-6. Funciones y métodos para crear y utilizar temporizadores (t es un objeto Temporizador)
Función Descripción

d3.timer( callback, after, start )

Devuelve una nueva instancia de temporizador. El temporizador invocará la llamada de retorno perpetuamente una vez por fotograma de animación. Cuando se invoque, se pasará a la llamada de retorno el tiempo aparente transcurrido desde que el temporizador empezó a funcionar (el tiempo aparente transcurrido no avanza mientras la ventana o pestaña está en segundo plano). El argumento numérico start puede contener una marca de tiempo, devuelta por d3.now(), en la que está previsto que comience el temporizador (por defecto es ahora). El argumento numérico after puede contener un retardo, en milisegundos, que se añadirá a la hora de inicio (por defecto es 0).

d3.timeout( callback, after, start )

Como d3.timer(), salvo que la llamada de retorno se invocará exactamente una vez.

d3.interval( callback, interval, start )

Similar a d3.timer(), salvo que la llamada de retorno sólo se invocará cada interval milisegundos.

t.stop()

Detiene este temporizador. No tiene efecto si el temporizador ya está parado.

d3.now()

Devuelve la hora actual, en milisegundos.

Ejemplo: Animaciones en tiempo real

Ejemplo 4-6 crea una animación suave actualizando un gráfico en cada repintado del navegador. El gráfico (véase la parte izquierda de laFigura 4-4) dibuja una línea (una curva de Lissajous5) que se desvanece lentamente a medida que pasa el tiempo. A diferencia de la mayoría de los demás ejemplos, este código no utiliza vinculación, ¡principalmente porque no hay ningún conjunto de datos que vincular! En su lugar, en cada paso temporal, se calcula la siguiente posición de la curva y se añade un nuevo elemento <line> al gráfico desde la posición anterior hasta la nueva. La opacidad de todos los elementos se reduce en un factor constante, y los elementos cuya opacidad haya bajado tanto que sean esencialmente invisibles se eliminan del gráfico. El valor actual de la opacidad se almacena en cada DOM Node como una nueva propiedad "falsa". Esto es opcional; en su lugar, podrías almacenar el valor en una estructura de datos independiente con clave por cada nodo (por ejemplo, utilizando d3.local(), que está pensada para este fin), o consultar el valor actual utilizando attr(), actualizarlo y restablecerlo.

Ejemplo 4-6. Animación en tiempo real (ver la parte izquierda de la Figura 4-4)
function makeLissajous() {
    var svg = d3.select( "#lissajous" );

    var a = 3.2, b = 5.9;                 // Lissajous frequencies
    var phi, omega = 2*Math.PI/10000;     // 10 seconds per period

    var crrX = 150+100, crrY = 150+0;
    var prvX = crrX, prvY = crrY;

    var timer = d3.timer( function(t) {
        phi = omega*t;

        crrX = 150+100*Math.cos(a*phi);
        crrY = 150+100*Math.sin(b*phi);

        svg.selectAll( "line" )
            .each( function() { this.bogus_opacity *= .99 } )
            .attr( "stroke-opacity",
                   function() { return this.bogus_opacity } )
            .filter( function() { return this.bogus_opacity<0.05 } )
            .remove();

        svg.append( "line" )
            .each( function() { this.bogus_opacity = 1.0 } )
            .attr( "x1", prvX ).attr( "y1", prvY )
            .attr( "x2", crrX ).attr( "y2", crrY )
            .attr( "stroke", "green" ).attr( "stroke-width", 2 );

        prvX = crrX;
        prvY = crrY;

        if( t > 120e3 ) { timer.stop(); } // after 120 seconds
    } );
}
dfti 0404
Figura 4-4. Animaciones: una figura de Lissajous (izquierda, ver Ejemplo 4-6), y un modelo de votante (derecha, ver Ejemplo 4-7)

Ejemplo: Suavizar actualizaciones periódicas con transiciones

En el ejemplo anterior, cada nuevo punto de datos a mostrar se calculaba en tiempo real. Eso no siempre es posible. Imagina que necesitas acceder a un servidor remoto para obtener datos. Puede que quieras sondearlo periódicamente, pero desde luego no para cada repintado. En cualquier caso, una obtención remota es siempre una llamada asíncrona y debe tratarse en consecuencia.

En una situación así, las transiciones pueden ayudar a crear una mejor experiencia de usuario, suavizando los periodos de tiempo entre las actualizaciones de la fuente de datos. En el Ejemplo 4-7, el servidor remoto se ha sustituido por una función local para simplificar el ejemplo, pero la mayoría de los conceptos se mantienen. El ejemplo implementa un modelo de votante simple:6 en cada paso temporal, cada elemento del grafo selecciona aleatoriamente uno de sus ocho vecinos y adopta su color. Sólo se llama a la función de actualización cada pocos segundos; mientras tanto, se utilizan transiciones D3 para actualizar el grafo sin problemas (véase la parte derecha de la Figura 4-4).

Ejemplo 4-7. Utilizar transiciones para suavizar las actualizaciones periódicas (ver la parte derecha de la Figura 4-4)
function makeVoters() {
    var n = 50, w=300/n, dt = 3000, svg = d3.select( "#voters" );

    var data = d3.range(n*n)                                      1
        .map( d => { return { x: d%n, y: d/n|0,
                              val: Math.random() } } );

    var sc = d3.scaleQuantize()                                   2
        .range( [ "white", "red", "black" ] );

    svg.selectAll( "rect" ).data( data ).enter().append( "rect" ) 3
        .attr( "x", d=>w*d.x ).attr( "y", d=>w*d.y )
        .attr( "width", w-1 ).attr( "height", w-1 )
        .attr( "fill", d => sc(d.val) );

    function update() {                                           4
        var nbs = [ [0,1], [0,-1], [ 1,0], [-1, 0],
                    [1,1], [1,-1], [-1,1], [-1,-1] ];
        return d3.shuffle( d3.range( n*n ) ).map( i => {
            var nb = nbs[ nbs.length*Math.random() | 0 ];
            var x = (data[i].x + nb[0] + n)%n;
            var y = (data[i].y + nb[1] + n)%n;
            data[i].val = data[ y*n + x ].val;
        } );
    }

    d3.interval( function() {                                     5
        update();
        svg.selectAll( "rect" ).data( data )
            .transition().duration(dt).delay((d,i)=>i*0.25*dt/(n*n))
            .attr( "fill", d => sc(d.val) ) }, dt );
}
1

Crea una matriz de n2 objetos. Cada objeto tiene un valor aleatorio entre 0 y 1, y también el conocimiento de sus coordenadas x e y en un cuadrado. (La expresión d/n|0 impar es una forma abreviada de truncar el cociente de la izquierda a un número entero: el operador OR de bits fuerza sus operandos a una representación entera, truncando los decimales en el proceso. Se trata de un modismo semicomún de JavaScript que merece la pena conocer).

2

El objeto devuelto por d3.scaleQuantize() es una instancia de unaescala de binning, que divide su dominio de entrada en bins de igual tamaño. Aquí, el dominio de entrada por defecto [0,1] se divide en tres intervalos de igual tamaño, uno para cada color. (Para más detalles sobre los objetos de escala, consulta el Capítulo 7.)

3

Vincula el conjunto de datos y, a continuación, crea un rectángulo para cada registro del conjunto de datos. Cada registro de datos contiene información sobre la posición del rectángulo, y el objeto escala se utiliza para asignar la propiedad valor de cada registro a un color.

4

La función de actualización real que calcula una nueva configuración cuando se llama. Visita cada elemento de la matriz en orden aleatorio. Para cada elemento, selecciona al azar uno de sus ocho vecinos y asigna el valor del vecino al elemento actual. (La finalidad de la aritmética es convertir entre el índice de la matriz del elemento y sus coordenadas(x, y) en la representación matricial, teniendo en cuenta las condiciones periódicas de contorno: si sales de la matriz por la izquierda, vuelves a entrar por la derecha y viceversa; lo mismo para arriba y abajo).

5

La función d3.interval() devuelve un temporizador que invoca la llamada de retorno especificada con una frecuencia configurable. En este caso, llama a la función update()cada dt milisegundos y luego actualiza los elementos del gráfico con los nuevos datos. Las actualizaciones se suavizan mediante una transición, que se retrasa según la posición del elemento en la matriz. El retraso es corto en comparación con la duración de la transición. El efecto es que la actualización barre de arriba abajo la figura .

1 Consulta la Referencia de Eventos MDN para más información.

2 Si quieres acceder a this en una llamada de retorno, debes utilizar la palabra clave function para definir la llamada de retorno; no puedes utilizar una función de flecha.

3 Son: screen, relativo al perímetro de la pantalla física; client, relativo al perímetro de la ventana del navegador; y page, relativo al perímetro del propio documento. Debido a la colocación de la ventana en la pantalla, y al desplazamiento de la página dentro del navegador, estas tres serán generalmente diferentes.

4 Si intentas poner en práctica el ejemplo actual sin utilizar la función D3 drag, puede que ocasionalmente observes un comportamiento espurio de la interfaz de usuario. Es probable que la acción por defecto del navegador interfiera con el comportamiento previsto. El remedio es llamar a d3.event.preventDefault() en el manejador mousemove. Para más información, consulta el Apéndice C.

5 Véase http://mathworld.wolfram.com/LissajousCurve.html.

6 Véase http://mathworld.wolfram.com/VoterModel.html.

Get D3 para impacientes now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.