Actualización del índice Z del elemento SVG con D3

81

¿Cuál es una forma eficaz de llevar un elemento SVG a la parte superior del orden z, utilizando la biblioteca D3?

Mi escenario específico es un gráfico circular que resalta (agregando un strokeal path) cuando el mouse está sobre una pieza determinada. El bloque de código para generar mi gráfico se encuentra a continuación:

svg.selectAll("path")
    .data(d)
  .enter().append("path")
    .attr("d", arc)
    .attr("class", "arc")
    .attr("fill", function(d) { return color(d.name); })
    .attr("stroke", "#fff")
    .attr("stroke-width", 0)
    .on("mouseover", function(d) {
        d3.select(this)
            .attr("stroke-width", 2)
            .classed("top", true);
            //.style("z-index", 1);
    })
    .on("mouseout", function(d) {
        d3.select(this)
            .attr("stroke-width", 0)
            .classed("top", false);
            //.style("z-index", -1);
    });

Probé algunas opciones, pero hasta ahora no tuve suerte. Usar style("z-index")y llamar a classedambos no funcionó.

La clase "superior" se define de la siguiente manera en mi CSS:

.top {
    fill: red;
    z-index: 100;
}

La filldeclaración está ahí para asegurarme de que sabía que se estaba encendiendo / apagando correctamente. Está.

Escuché que usar sortes una opción, pero no tengo claro cómo se implementaría para llevar el elemento "seleccionado" a la parte superior.

ACTUALIZAR:

Arreglé mi situación particular con el siguiente código, que agrega un nuevo arco al SVG en el mouseoverevento para mostrar un punto culminante.

svg.selectAll("path")
    .data(d)
  .enter().append("path")
    .attr("d", arc)
    .attr("class", "arc")
    .style("fill", function(d) { return color(d.name); })
    .style("stroke", "#fff")
    .style("stroke-width", 0)
    .on("mouseover", function(d) {
        svg.append("path")
          .attr("d", d3.select(this).attr("d"))
          .attr("id", "arcSelection")
          .style("fill", "none")
          .style("stroke", "#fff")
          .style("stroke-width", 2);
    })
    .on("mouseout", function(d) {
        d3.select("#arcSelection").remove();
    });
Mono malvado del armario
fuente

Respuestas:

52

Una de las soluciones presentadas por el desarrollador es: "utilizar el operador de clasificación de D3 para reordenar los elementos". (ver https://github.com/mbostock/d3/issues/252 )

En este sentido, uno podría ordenar los elementos comparando sus datos, o posiciones si fueran elementos sin datos:

.on("mouseover", function(d) {
    svg.selectAll("path").sort(function (a, b) { // select the parent and sort the path's
      if (a.id != d.id) return -1;               // a is not the hovered element, send "a" to the back
      else return 1;                             // a is the hovered element, bring "a" to the front
  });
})
Futurend
fuente
Esto no solo funciona en el orden Z, sino que tampoco altera el arrastre que acaba de comenzar con el elemento que se eleva.
Oliver Bock
3
tampoco me funciona a mí. a, b parece ser solo los datos, no el elemento de selección d3 o el elemento DOM
nkint
De hecho, al igual que para @nkint, no funcionó al comparar a.idy d.id. La solución es comparar directamente ay d.
Peace Makes Plenty
Esto no funcionará adecuadamente para una gran cantidad de elementos secundarios. Es mejor volver a renderizar el elemento elevado en un archivo <g>.
tribalvibes
1
Alternativamente, el svg:useelemento puede apuntar dinámicamente al elemento elevado. Consulte stackoverflow.com/a/6289809/292085 para obtener una solución.
tribalvibes
91

Como se explica en las otras respuestas, SVG no tiene una noción de índice z. En cambio, el orden de los elementos en el documento determina el orden en el dibujo.

Además de reordenar los elementos manualmente, existe otra forma para determinadas situaciones:

Al trabajar con D3, a menudo tiene ciertos tipos de elementos que siempre deben dibujarse encima de otros tipos de elementos .

Por ejemplo, al diseñar gráficos, los enlaces siempre deben colocarse debajo de los nodos. De manera más general, algunos elementos de fondo generalmente deben colocarse debajo de todo lo demás, mientras que algunos resaltados y superposiciones deben colocarse arriba.

Si tiene este tipo de situación, descubrí que la mejor manera de hacerlo es crear elementos de grupo principal para esos grupos de elementos. En SVG, puede usar el gelemento para eso. Por ejemplo, si tiene enlaces que siempre deben colocarse debajo de los nodos, haga lo siguiente:

svg.append("g").attr("id", "links")
svg.append("g").attr("id", "nodes")

Ahora, cuando pinte sus enlaces y nodos, seleccione lo siguiente (los selectores que comienzan con #referencia al ID del elemento):

svg.select("#links").selectAll(".link")
// add data, attach elements and so on

svg.select("#nodes").selectAll(".node")
// add data, attach elements and so on

Ahora, todos los enlaces siempre se agregarán estructuralmente antes de todos los elementos del nodo. Por lo tanto, el SVG mostrará todos los enlaces debajo de todos los nodos, sin importar con qué frecuencia y en qué orden agregue o elimine elementos. Por supuesto, todos los elementos del mismo tipo (es decir, dentro del mismo contenedor) aún estarán sujetos al orden en que se agregaron.

notan3xit
fuente
1
Me encontré con esto con la idea en mi cabeza de que podía mover un elemento hacia arriba, cuando mi problema real era la organización de mis elementos en grupos. Si su problema "real" es mover elementos individuales, entonces el método de clasificación descrito en la respuesta anterior es un buen camino a seguir.
John David Five
¡Increíble! Gracias, notan3xit, ¡me salvaste el día! Solo para agregar una idea para otros: si necesita agregar elementos en un orden diferente al orden de visibilidad deseado, puede crear grupos en el orden de visibilidad adecuado (incluso antes de agregarles elementos) y luego agregar elementos a estos grupos en cualquier orden.
Olga Gnatenko
1
Este es el enfoque correcto. Obvio una vez que lo piensas.
Dave
Para mí, svg.select ('# links') ... tuvo que ser reemplazado por d3.select ('# links') ... para que funcione. Quizás esto ayude a otros.
intercambio
26

Dado que SVG no tiene índice Z pero usa el orden de los elementos DOM, puede traerlo al frente de la siguiente manera:

this.parentNode.appendChild(this);

Entonces puede, por ejemplo, hacer uso de insertBefore para volver a colocarlo mouseout. Sin embargo, esto requiere que pueda apuntar al nodo hermano que su elemento debe insertarse antes.

DEMO: Eche un vistazo a este JSFiddle

Swenedo
fuente
Esta sería una gran solución si no fuera porque Firefox no puede representar ningún :hoverestilo. Tampoco creo que la función ìnsert al final sea necesaria para este caso de uso.
John Slegers
@swenedo, tu solución funciona muy bien para mí y trae el elemento al frente. En cuanto a llevarlo al fondo, estoy un poco apilado y no sé cómo escribirlo correctamente. ¿Puede, por favor, publicar un ejemplo de cómo puede llevar el objeto al fondo? Gracias.
Mike B.
1
@MikeB. Claro, acabo de actualizar mi respuesta. Me di cuenta de que la insertfunción de d3 probablemente no sea la mejor manera de hacerlo, porque crea un nuevo elemento. Solo queremos mover un elemento existente, por lo tanto, lo uso insertBeforeen este ejemplo.
swenedo
Intenté usar este método, pero desafortunadamente no funciona en IE11. ¿Tiene una solución alternativa para este navegador?
Antonio Giovanni Schiavone
13

SVG no tiene índice z. El orden Z está dictado por el orden de los elementos DOM de SVG en su contenedor.

Por lo que pude ver (y lo he intentado un par de veces en el pasado), D3 no proporciona métodos para separar y volver a unir un solo elemento para llevarlo al frente o lo que sea.

Existe un .order()método que reorganiza los nodos para que coincidan con el orden en que aparecen en la selección. En su caso, debe traer un solo elemento al frente. Entonces, técnicamente, podría recurrir a la selección con el elemento deseado al frente (o al final, no puedo recordar cuál es el más alto) y luego llamarlo order().

O bien, puede omitir d3 para esta tarea y usar JS simple (o jQuery) para volver a insertar ese único elemento DOM.

Meetamit
fuente
5
Tenga en cuenta que hay problemas con la eliminación / inserción de elementos dentro de un controlador de mouseover en algunos navegadores, puede provocar que los eventos de mouseout no se envíen correctamente, por lo que recomiendo probar esto en todos los navegadores para asegurarse de que funcione como espera.
Erik Dahlström
¿Alterar el orden no cambiaría también el diseño de mi gráfico circular? También estoy investigando jQuery para aprender cómo volver a insertar elementos.
Evil Closet Monkey
Actualicé mi pregunta con la solución que elegí, que en realidad no utiliza el núcleo de la pregunta original. Si ve algo realmente malo en esta solución, ¡agradeceré sus comentarios! Gracias por su comprensión de la pregunta original.
Evil Closet Monkey
Parece un enfoque razonable, principalmente gracias al hecho de que la forma superpuesta no tiene relleno. Si lo hiciera, esperaría que "robara" el evento del mouse en el momento en que apareció. Y creo que el punto de @ ErikDahlström sigue en pie: esto podría ser un problema en algunos navegadores (IE9 principalmente). Para mayor "seguridad", puede agregar .style('pointer-events', 'none')a la ruta superpuesta. Desafortunadamente, esta propiedad no hace nada en IE, pero aún así ....
meetamit
@EvilClosetMonkey Hola ... Esta pregunta recibe muchas visitas, y creo que la respuesta de futurend a continuación es más útil que la mía. Entonces, tal vez considere cambiar la respuesta aceptada a futurend, para que parezca más arriba.
meetamit
4

Implementé la solución de futurend en mi código y funcionó, pero con la gran cantidad de elementos que estaba usando, fue muy lento. Aquí está el método alternativo con jQuery que funcionó más rápido para mi visualización particular. Se basa en los svgs que desea que tengan una clase en común (en mi ejemplo, la clase se indica en mi conjunto de datos como d.key). En mi código hay una <g>con la clase "ubicaciones" que contiene todos los SVG que estoy reorganizando.

.on("mouseover", function(d) {
    var pts = $("." + d.key).detach();
    $(".locations").append(pts);
 });

Entonces, cuando pasa el mouse sobre un punto de datos en particular, el código encuentra todos los demás puntos de datos con elementos DOM de SVG con esa clase en particular. Luego, separa y vuelve a insertar los elementos SVG DOM asociados con esos puntos de datos.

lk145
fuente
4

Quería ampliar lo que respondió @ notan3xit en lugar de escribir una respuesta completamente nueva (pero no tengo suficiente reputación).

Otra forma de resolver el problema del orden de los elementos es usar 'insertar' en lugar de 'agregar' al dibujar. De esa manera, las rutas siempre se colocarán juntas antes de los otros elementos svg (esto supone que su código ya hace enter () para los enlaces antes de enter () para los otros elementos svg).

d3 insert api: https://github.com/mbostock/d3/wiki/Selections#insert

Ali
fuente
1

Me tomó años encontrar cómo modificar el orden Z en un SVG existente. Realmente lo necesitaba en el contexto de d3.brush con comportamiento de información sobre herramientas. Para que las dos funciones funcionen bien juntas ( http://wrobstory.github.io/2013/11/D3-brush-and-tooltip.html ), necesita que d3.brush sea el primero en el orden Z (Primero se dibuja en el lienzo, luego se cubre con el resto de los elementos SVG) y capturará todos los eventos del mouse, sin importar lo que esté encima (con índices Z más altos).

La mayoría de los comentarios del foro dicen que debes agregar el d3.brush primero en tu código, luego tu código de "dibujo" SVG. Pero para mí no fue posible porque cargué un archivo SVG externo. Puede agregar fácilmente el pincel en cualquier momento y modificar el orden Z más adelante con:

d3.select("svg").insert("g", ":first-child");

En el contexto de una configuración de d3.brush, se verá así:

brush = d3.svg.brush()
    .x(d3.scale.identity().domain([1, width-1]))
    .y(d3.scale.identity().domain([1, height-1]))
    .clamp([true,true])
    .on("brush", function() {
      var extent = d3.event.target.extent();
      ...
    });
d3.select("svg").insert("g", ":first-child");
  .attr("class", "brush")
  .call(brush);

API de función insert () de d3.js: https://github.com/mbostock/d3/wiki/Selections#insert

¡Espero que esto ayude!

mskr182
fuente
0

Versión 1

En teoría, lo siguiente debería funcionar bien.

El código CSS:

path:hover {
    stroke: #fff;
    stroke-width : 2;
}

Este código CSS agregará un trazo a la ruta seleccionada.

El código JS:

svg.selectAll("path").on("mouseover", function(d) {
    this.parentNode.appendChild(this);
});

Este código JS primero elimina la ruta del árbol DOM y luego lo agrega como el último hijo de su padre. Esto asegura que la ruta se dibuje encima de todos los demás hijos del mismo padre.

En la práctica, este código funciona bien en Chrome, pero se rompe en otros navegadores. Lo probé en Firefox 20 en mi máquina Linux Mint y no pude hacerlo funcionar. De alguna manera, Firefox no activa los :hoverestilos y no he encontrado una manera de solucionarlo.


Versión 2

Entonces se me ocurrió una alternativa. Puede ser un poco 'sucio', pero al menos funciona y no requiere recorrer todos los elementos (como algunas de las otras respuestas).

El código CSS:

path.hover {
    stroke: #fff;
    stroke-width : 2;
}

En lugar de usar el :hoverpseudoelector, uso una .hoverclase

El código JS:

svg.selectAll(".path")
   .on("mouseover", function(d) {
       d3.select(this).classed('hover', true);
       this.parentNode.appendChild(this);
   })
   .on("mouseout", function(d) {
       d3.select(this).classed('hover', false);
   })

Al pasar el mouse, agrego la .hoverclase a mi ruta. Al mouseout, lo elimino. Como en el primer caso, el código también elimina la ruta del árbol DOM y luego la agrega como el último hijo de su padre.

John Slegers
fuente
0

Puede hacer esto al pasar el mouse por encima. Puede colocarlo en la parte superior.

d3.selection.prototype.bringElementAsTopLayer = function() {
       return this.each(function(){
       this.parentNode.appendChild(this);
   });
};

d3.selection.prototype.pushElementAsBackLayer = function() { 
return this.each(function() { 
    var firstChild = this.parentNode.firstChild; 
    if (firstChild) { 
        this.parentNode.insertBefore(this, firstChild); 
    } 
}); 

};

nodes.on("mouseover",function(){
  d3.select(this).bringElementAsTopLayer();
});

Si quieres empujar hacia atrás

nodes.on("mouseout",function(){
   d3.select(this).pushElementAsBackLayer();
});
RamiReddy P
fuente