Aktualizacja indeksu Z elementu SVG za pomocą D3

81

Jaki jest skuteczny sposób na umieszczenie elementu SVG na szczycie kolejności z przy użyciu biblioteki D3?

Mój konkretny scenariusz to wykres kołowy, który podkreśla (dodając a strokedo path), kiedy wskaźnik myszy znajduje się nad danym kawałkiem. Blok kodu do generowania mojego wykresu znajduje się poniżej:

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);
    });

Wypróbowałem kilka opcji, ale na razie bez powodzenia. Używanie style("z-index")i wywoływanie classedobu nie działało.

„Najwyższa” klasa jest zdefiniowana w moim CSS w następujący sposób:

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

fillStwierdzenie jest tam, aby upewnić się, że wiedział, że włączanie / wyłączanie poprawnie. To jest.

Słyszałem, że użycie sortjest opcją, ale nie jestem pewien, w jaki sposób zostanie zaimplementowana w celu przeniesienia „wybranego” elementu na górę.

AKTUALIZACJA:

Naprawiłem moją szczególną sytuację za pomocą następującego kodu, który dodaje nowy łuk do pliku SVG na mouseoverwydarzeniu, aby pokazać wyróżnienie.

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();
    });
Zła małpa w szafie
źródło

Odpowiedzi:

52

Jednym z rozwiązań przedstawionych przez dewelopera jest: „użyj operatora sortowania D3, aby zmienić kolejność elementów”. (patrz https://github.com/mbostock/d3/issues/252 )

W tym świetle można posortować elementy, porównując ich dane lub pozycje, jeśli są elementami bez danych:

.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
źródło
Działa to nie tylko w kolejności Z, ale także nie zakłóca żadnego przeciągania, które właśnie rozpoczęło się z podniesionym elementem.
Oliver Bock,
3
na mnie też nie działa. a, b wydaje się być tylko danymi, a nie elementem selekcji d3 lub elementem DOM
nkint
Rzeczywiście, podobnie jak w przypadku @nkint, nie działało to podczas porównywania a.idi d.id. Rozwiązaniem jest bezpośrednie porównanie ai d.
Peace Makes Plenty
Nie będzie to działać odpowiednio w przypadku dużej liczby elementów podrzędnych. Lepiej jest ponownie wyrenderować podniesiony element w kolejnym <g>.
tribalvibes
1
Alternatywnie svg:useelement może wskazywać dynamicznie na podniesiony element. Rozwiązanie znajdziesz na stackoverflow.com/a/6289809/292085 .
tribalvibes
91

Jak wyjaśniono w innych odpowiedziach, SVG nie ma pojęcia indeksu z. Zamiast tego kolejność elementów w dokumencie określa kolejność na rysunku.

Oprócz ręcznej zmiany kolejności elementów istnieje inny sposób w określonych sytuacjach:

Pracując z D3 często masz pewne typy elementów, które zawsze powinny być narysowane na innych typach elementów .

Na przykład podczas układania wykresów linki należy zawsze umieszczać poniżej węzłów. Mówiąc bardziej ogólnie, niektóre elementy tła zwykle należy umieścić pod wszystkim innym, a niektóre podświetlenia i nakładki należy umieścić powyżej.

Jeśli masz tego rodzaju sytuację, stwierdziłem, że tworzenie elementów grupy nadrzędnej dla tych grup elementów jest najlepszym sposobem. W SVG możesz użyć do tego gelementu. Na przykład, jeśli masz łącza, które zawsze powinny być umieszczane pod węzłami, wykonaj następujące czynności:

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

Teraz, kiedy malujesz swoje linki i węzły, wybierz w następujący sposób (selektory rozpoczynające się #od identyfikatora elementu):

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

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

Teraz wszystkie linki będą zawsze dołączane strukturalnie przed wszystkimi elementami węzła. W ten sposób SVG pokaże wszystkie linki poniżej wszystkich węzłów, bez względu na to, jak często i w jakiej kolejności dodasz lub usuniesz elementy. Oczywiście wszystkie elementy tego samego typu (czyli w tym samym kontenerze) nadal będą podlegały kolejności, w jakiej zostały dodane.

notan3xit
źródło
1
Wpadłem na to z myślą, że mogę przesunąć jedną pozycję w górę, podczas gdy moim prawdziwym problemem było zorganizowanie moich elementów w grupy. Jeśli Twoim „prawdziwym” problemem jest przesuwanie poszczególnych elementów, metoda sortowania opisana w poprzedniej odpowiedzi jest dobrym rozwiązaniem.
John David Five,
Niesamowite! Dziękuję, notan3xit, uratowałeś mi dzień! Wystarczy dodać pomysł dla innych - jeśli potrzebujesz dodać elementy w kolejności innej niż pożądana kolejność widoczności, możesz po prostu utworzyć grupy w odpowiedniej kolejności widoczności (nawet przed dodaniem do nich jakichkolwiek elementów), a następnie dołączyć do nich elementy grupy w dowolnej kolejności.
Olga Gnatenko
1
To jest właściwe podejście. Oczywiste, kiedy się nad tym zastanowić.
Dave
Dla mnie svg.select ('# links') ... musiało zostać zastąpione przez d3.select ('# links') ... aby działało. Może to pomoże innym.
wymiana
26

Ponieważ SVG nie ma indeksu Z, ale używa kolejności elementów DOM, możesz go przenieść na pierwszy plan przez:

this.parentNode.appendChild(this);

Możesz wtedy np. Skorzystać z insertBefore przed ponownym założeniem mouseout. Wymaga to jednak, abyś był w stanie wskazać węzeł rodzeństwa, który chcesz wstawić wcześniej.

DEMO: Spójrz na to JSFiddle

swenedo
źródło
Byłoby to świetne rozwiązanie, gdyby nie Firefox nie renderujący żadnych :hoverstylów. Nie sądzę również, aby funkcja ìnsert na końcu była wymagana w tym przypadku użycia.
John Slegers
@swenedo, twoje rozwiązanie działa dla mnie świetnie i wysuwa element na pierwszy plan. Jeśli chodzi o przeniesienie go na koniec, jestem trochę ułożony i nie wiem, jak to poprawnie napisać. Czy możesz, proszę, zamieścić przykład, w jaki sposób możesz przenieść przedmiot na tył. Dzięki.
Mike B.
1
@MikeB. Jasne, właśnie zaktualizowałem moją odpowiedź. Zdałem sobie sprawę, że insertfunkcja d3 prawdopodobnie nie jest najlepszym rozwiązaniem, ponieważ tworzy nowy element. Chcemy tylko przenieść istniejący element, dlatego insertBeforezamiast tego używam w tym przykładzie.
swenedo
Próbowałem użyć tej metody, ale niestety nie działa w IE11. Czy masz jakieś obejście dla tej przeglądarki?
Antonio Giovanni Schiavone
13

SVG nie wykonuje indeksu Z. Porządek Z jest podyktowany kolejnością elementów SVG DOM w ich kontenerze.

O ile mogłem powiedzieć (i próbowałem tego kilka razy w przeszłości), D3 nie zapewnia metod odłączania i ponownego mocowania pojedynczego elementu w celu przesunięcia go na przód lub tak dalej.

Istnieje .order()metoda , która przetasowuje węzły w celu dopasowania ich do kolejności, w jakiej pojawiają się w zaznaczeniu. W twoim przypadku musisz przenieść jeden element na przód. Więc technicznie rzecz biorąc, możesz dokonać wyboru z żądanym elementem na początku (lub na końcu, nie pamiętając, który jest najwyższy), a następnie go wywołać order().

Możesz też pominąć d3 w tym zadaniu i użyć zwykłego JS (lub jQuery), aby ponownie wstawić ten pojedynczy element DOM.

meetamit
źródło
5
Zwróć uwagę, że w niektórych przeglądarkach występują problemy z usuwaniem / wstawianiem elementów wewnątrz modułu obsługi wskaźnika myszy, może to prowadzić do nieprawidłowego wysyłania zdarzeń myszy, więc zalecam wypróbowanie tego we wszystkich przeglądarkach, aby upewnić się, że działa zgodnie z oczekiwaniami.
Erik Dahlström
Czy zmiana kolejności nie zmieniłaby również układu mojego wykresu kołowego? Przeszukuję również jQuery, aby dowiedzieć się, jak ponownie wstawiać elementy.
Evil Closet Monkey
Zaktualizowałem moje pytanie wybranym przeze mnie rozwiązaniem, które w rzeczywistości nie wykorzystuje sedna pierwotnego pytania. Jeśli zauważysz coś naprawdę złego w tym rozwiązaniu, mile widziane komentarze! Dziękuję za wgląd w pierwotne pytanie.
Evil Closet Monkey
Wydaje się, że to rozsądne podejście, głównie dzięki temu, że nałożony kształt nie ma wypełnienia. Gdyby tak było, spodziewałbym się, że „ukradnie” zdarzenie myszy w momencie, gdy się pojawi. I myślę, że argument @ ErikDahlström nadal jest aktualny: może to nadal stanowić problem w niektórych przeglądarkach (głównie IE9). Aby uzyskać dodatkowe „bezpieczeństwo”, można dodać .style('pointer-events', 'none')do nałożonej ścieżki. Niestety ta właściwość nic nie robi w IE, ale mimo to ....
meetamit
@EvilClosetMonkey Cześć ... To pytanie ma wiele wyświetleń i uważam, że poniższa odpowiedź futurend jest bardziej pomocna niż moja. Więc może rozważ zmianę akceptowanej odpowiedzi na futurend, aby wydawała się wyżej.
spotkanie
4

Wdrożyłem rozwiązanie futurend w swoim kodzie i działało, ale przy dużej liczbie elementów, których używałem, działało bardzo wolno. Oto alternatywna metoda wykorzystująca jQuery, która działała szybciej w przypadku mojej konkretnej wizualizacji. Polega na tym, że pliki SVG, które chcesz, mają wspólną klasę (w moim przykładzie klasa jest zapisana w moim zestawie danych jako d.key). W moim kodzie znajduje się <g>z klasą „lokalizacje”, która zawiera wszystkie pliki SVG, które reorganizuję.

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

Więc kiedy najedziesz kursorem na konkretny punkt danych, kod znajdzie wszystkie inne punkty danych z elementami SVG DOM z tą konkretną klasą. Następnie odłącza i ponownie wstawia elementy SVG DOM powiązane z tymi punktami danych.

lk145
źródło
4

Chciałem rozszerzyć to, na co odpowiedział @ notan3xit, zamiast pisać całą nową odpowiedź (ale nie mam wystarczającej reputacji).

Innym sposobem rozwiązania problemu z kolejnością elementów jest użycie opcji „wstaw” zamiast „dołącz” podczas rysowania. W ten sposób ścieżki będą zawsze umieszczane razem przed innymi elementami svg (zakłada to, że twój kod już wykonuje enter () dla linków przed enter () dla innych elementów svg).

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

Ali
źródło
1

Wieki zajęło mi znalezienie sposobu na poprawienie kolejności Z w istniejącym SVG. Naprawdę potrzebowałem tego w kontekście d3.brush z zachowaniem podpowiedzi. Aby te dwie funkcje dobrze ze sobą współpracowały ( http://wrobstory.github.io/2013/11/D3-brush-and-tooltip.html ), musisz mieć d3.brush jako pierwszy w kolejności Z (Pierwsza do narysowania na płótnie, a następnie zakryta przez pozostałe elementy SVG) i przechwytuje wszystkie zdarzenia myszy, bez względu na to, co jest na wierzchu (z wyższymi indeksami Z).

Większość komentarzy na forum mówi, że najpierw należy dodać plik d3.brush do kodu, a następnie kod „rysujący” SVG. Ale dla mnie nie było to możliwe, ponieważ załadowałem zewnętrzny plik SVG. Możesz łatwo dodać pędzel w dowolnym momencie i zmienić kolejność Z później za pomocą:

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

W kontekście konfiguracji d3.brush będzie wyglądać następująco:

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 funkcji insert () d3.js: https://github.com/mbostock/d3/wiki/Selections#insert

Mam nadzieję że to pomoże!

mskr182
źródło
0

Wersja 1

Teoretycznie poniższe powinny działać dobrze.

Kod CSS:

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

Ten kod CSS doda obrys do wybranej ścieżki.

Kod JS:

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

Ten kod JS najpierw usuwa ścieżkę z drzewa DOM, a następnie dodaje ją jako ostatnie dziecko swojego rodzica. Daje to pewność, że ścieżka jest rysowana na wszystkich innych elementach potomnych tego samego rodzica.

W praktyce ten kod działa dobrze w Chrome, ale psuje się w niektórych innych przeglądarkach. Wypróbowałem to w Firefoksie 20 na moim komputerze z Linux Mint i nie mogłem go uruchomić. W jakiś sposób Firefox nie uruchamia :hoverstylów i nie znalazłem sposobu, aby to naprawić.


Wersja 2

Więc wymyśliłem alternatywę. Może być trochę „brudny”, ale przynajmniej działa i nie wymaga zapętlania wszystkich elementów (jak niektóre inne odpowiedzi).

Kod CSS:

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

Zamiast korzystać z :hoverpseudoselektora, używam .hoverklasy

Kod 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);
   })

Po najechaniu myszą dodaję .hoverklasę do mojej ścieżki. Po wyprowadzeniu myszy usuwam go. Podobnie jak w pierwszym przypadku, kod usuwa również ścieżkę z drzewa DOM, a następnie dodaje ją jako ostatnie dziecko swojego rodzica.

John Slegers
źródło
0

Możesz to zrobić Po najechaniu myszą Możesz wyciągnąć go do góry.

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();
});

Jeśli chcesz odepchnąć

nodes.on("mouseout",function(){
   d3.select(this).pushElementAsBackLayer();
});
RamiReddy P
źródło