Zmień rozmiar svg, gdy rozmiar okna zostanie zmieniony w d3.js

183

Rysuję wykres rozrzutu za pomocą d3.js. Za pomocą tego pytania:
Uzyskaj rozmiar ekranu, bieżącą stronę internetową i okno przeglądarki

Korzystam z tej odpowiedzi:

var w = window,
    d = document,
    e = d.documentElement,
    g = d.getElementsByTagName('body')[0],
    x = w.innerWidth || e.clientWidth || g.clientWidth,
    y = w.innerHeight|| e.clientHeight|| g.clientHeight;

Jestem w stanie dopasować moją fabułę do okna użytkownika w następujący sposób:

var svg = d3.select("body").append("svg")
        .attr("width", x)
        .attr("height", y)
        .append("g");

Teraz chciałbym, żeby coś zmieniło rozmiar działki, gdy użytkownik zmieni rozmiar okna.

PS: Nie używam jQuery w moim kodzie.

mthpvg
źródło

Odpowiedzi:

295

Poszukaj „responsive SVG”. Utworzenie responsywnego SVG jest dość proste i nie musisz się już martwić o rozmiary.

Oto jak to zrobiłem:

d3.select("div#chartId")
   .append("div")
   // Container class to make it responsive.
   .classed("svg-container", true) 
   .append("svg")
   // Responsive SVG needs these 2 attributes and no width and height attr.
   .attr("preserveAspectRatio", "xMinYMin meet")
   .attr("viewBox", "0 0 600 400")
   // Class to make it responsive.
   .classed("svg-content-responsive", true)
   // Fill with a rectangle for visualization.
   .append("rect")
   .classed("rect", true)
   .attr("width", 600)
   .attr("height", 400);
.svg-container {
  display: inline-block;
  position: relative;
  width: 100%;
  padding-bottom: 100%; /* aspect ratio */
  vertical-align: top;
  overflow: hidden;
}
.svg-content-responsive {
  display: inline-block;
  position: absolute;
  top: 10px;
  left: 0;
}

svg .rect {
  fill: gold;
  stroke: steelblue;
  stroke-width: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<div id="chartId"></div>

Uwaga: wszystko na obrazie SVG będzie skalowane wraz z szerokością okna. Obejmuje to szerokość obrysu i rozmiary czcionek (nawet te ustawione za pomocą CSS). Jeśli nie jest to pożądane, poniżej znajdziesz więcej zaangażowanych alternatywnych rozwiązań.

Więcej informacji / tutoriale:

http://thenewcode.com/744/Make-SVG-Responsive

http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php

cminatti
źródło
8
Jest to o wiele bardziej eleganckie i wykorzystuje podejście do skalowania kontenerów, które powinno znajdować się w każdym zestawie narzędzi dla programistów front-end (może być używane do skalowania czegokolwiek w określonym współczynniku proporcji). Powinna być najlepsza odpowiedź.
kontur
13
Aby dodać do tego: viewBoxatrybut należy ustawić na odpowiednią wysokość i szerokość wykresu SVG: .attr("viewBox","0 0 " + width + " " + height) gdzie widthi gdzie heightindziej. To znaczy, chyba że chcesz, aby Twój plik SVG został przycięty przez pole widoku. Możesz także zaktualizować padding-bottomparametr w swoim css, aby dopasować proporcje elementu svg. Doskonała odpowiedź - bardzo pomocna i działa na ucztę. Dzięki!
Andrew Guy,
13
W oparciu o ten sam pomysł, interesujące byłoby udzielenie odpowiedzi, która nie wymaga zachowania współczynnika kształtu. Pytanie dotyczyło posiadania elementu svg, który wciąż pokrywa pełne okno przy zmianie rozmiaru, co w większości przypadków oznacza inny współczynnik kształtu.
Nicolas Le Thierry d'Ennequin
37
Tylko uwaga dotycząca UX: w niektórych przypadkach użycie wykresu d3 opartego na SVG jako zasobu o stałym współczynniku proporcji nie jest idealne. Wykresy, które wyświetlają tekst lub są dynamiczne lub ogólnie bardziej przypominają odpowiedni interfejs użytkownika niż zasób, mogą zyskać na renderowaniu przy użyciu zaktualizowanych wymiarów. Na przykład viewBox zmusza czcionki osi i tiki do skalowania w górę / w dół, potencjalnie wyglądając niezręcznie w porównaniu z tekstem HTML. W przeciwieństwie do tego, ponowne renderowanie pozwala wykresowi zachować spójny rozmiar, a także daje możliwość dodawania lub usuwania kleszczy.
Karim Hernandez
1
Dlaczego top: 10px;? Wydaje mi się to nieprawidłowe i zapewnia przesunięcie. Ustawienie 0px działa dobrze.
TimZaman
43

Użyj window.onresize :

function updateWindow(){
    x = w.innerWidth || e.clientWidth || g.clientWidth;
    y = w.innerHeight|| e.clientHeight|| g.clientHeight;

    svg.attr("width", x).attr("height", y);
}
d3.select(window).on('resize.updatesvg', updateWindow);

http://jsfiddle.net/Zb85u/1/

Adam Pearce
źródło
11
To nie jest dobra odpowiedź, ponieważ wiąże się z przenoszeniem na zdarzenia DOM ... Znacznie lepszym rozwiązaniem jest użycie tylko d3 i css
alem0lars
@ alem0lars dość zabawnie, wersja css / d3 nie działała dla mnie, a ta działała ...
Christian
32

AKTUALIZACJA wystarczy użyć nowej metody z @cminatti


stara odpowiedź do celów historycznych

IMO lepiej jest używać select () i on (), ponieważ w ten sposób możesz mieć wiele programów obsługi zdarzeń zmiany rozmiaru ... po prostu nie denerwuj się

d3.select(window).on('resize', resize); 

function resize() {
    // update width
    width = parseInt(d3.select('#chart').style('width'), 10);
    width = width - margin.left - margin.right;

    // resize the chart
    x.range([0, width]);
    d3.select(chart.node().parentNode)
        .style('height', (y.rangeExtent()[1] + margin.top + margin.bottom) + 'px')
        .style('width', (width + margin.left + margin.right) + 'px');

    chart.selectAll('rect.background')
        .attr('width', width);

    chart.selectAll('rect.percent')
        .attr('width', function(d) { return x(d.percent); });

    // update median ticks
    var median = d3.median(chart.selectAll('.bar').data(), 
        function(d) { return d.percent; });

    chart.selectAll('line.median')
        .attr('x1', x(median))
        .attr('x2', x(median));


    // update axes
    chart.select('.x.axis.top').call(xAxis.orient('top'));
    chart.select('.x.axis.bottom').call(xAxis.orient('bottom'));

}

http://eyeseast.github.io/visible-data/2013/08/28/responsive-charts-with-d3/

slf
źródło
Nie mogę się mniej zgodzić. Metoda cminatti będzie działać na wszystkich plikach d3js, z którymi mamy do czynienia. Ta metoda transformacji przy zmianie rozmiaru na wykresie jest nadmierna. Ponadto należy go przepisać dla każdej możliwej mapy, którą moglibyśmy wymyślić. Powyższa metoda działa na wszystko. Wypróbowałem 4 przepisy, w tym rodzaj przeładowania, o którym wspomniałeś, i żaden z nich nie działa na wykresach wielopowierzchniowych, takich jak używane w kubizmie.
Eamonn Kenny
1
@EamonnKenny Nie mogę się z tobą więcej zgodzić. Inna odpowiedź na 7 miesięcy w przyszłości jest lepsza :)
slf 15.03.16
W przypadku tej metody: „d3.select (okno) .on” Nie mogłem znaleźć „d3.select (okno) .off” lub „d3.select (okno) .unbind”
kg-morza
Ta odpowiedź nie jest gorsza. Ich odpowiedź przeskaluje każdy aspekt wykresu (w tym tiki, oś, linie i adnotacje tekstowe), które mogą wyglądać niezręcznie, w niektórych rozmiarach ekranu, złe w innych i nieczytelne w ekstremalnych rozmiarach. Uważam, że lepiej jest zmienić rozmiar przy użyciu tej metody (lub, w przypadku bardziej nowoczesnych rozwiązań, @Angu Agarwal).
Rúnar Berg
12

To trochę brzydkie, jeśli kod zmiany rozmiaru jest prawie tak długi, jak kod do budowy wykresu na pierwszym miejscu. Więc zamiast zmieniać rozmiar każdego elementu istniejącego wykresu, dlaczego po prostu nie załadować go ponownie? Oto jak to dla mnie działało:

function data_display(data){
   e = document.getElementById('data-div');
   var w = e.clientWidth;
   // remove old svg if any -- otherwise resizing adds a second one
   d3.select('svg').remove();
   // create canvas
   var svg = d3.select('#data-div').append('svg')
                                   .attr('height', 100)
                                   .attr('width', w);
   // now add lots of beautiful elements to your graph
   // ...
}

data_display(my_data); // call on page load

window.addEventListener('resize', function(event){
    data_display(my_data); // just call it again...
}

Kluczową linią jest d3.select('svg').remove();. W przeciwnym razie każda zmiana rozmiaru doda kolejny element SVG poniżej poprzedniego.

Raik
źródło
To absolutnie zabije wydajność, jeśli przerysujesz cały element przy każdym zdarzeniu zmiany rozmiaru okna
sdemurjian
2
Ale to prawda: podczas renderowania możesz określić, czy powinieneś mieć wstawioną oś, ukryć legendę, robić inne rzeczy w oparciu o dostępną treść. Używając wyłącznie CSS / viewbox, wszystko sprawia, że ​​wizualizacja wygląda na zgaszoną. Dobry dla obrazów, zły dla danych.
Chris Knoll
8

Obowiązujące układy po prostu ustawienie atrybutów „wysokość” i „szerokość” nie będzie działać w celu ponownego wyśrodkowania / przeniesienia wykresu do kontenera svg. Istnieje jednak bardzo prosta odpowiedź, która działa dla układów siły znalezionych tutaj . W podsumowaniu:

Użyj tego samego (dowolnego) wydarzenia, które ci się podoba.

window.on('resize', resize);

Zakładając, że masz zmienne svg i force:

var svg = /* D3 Code */;
var force = /* D3 Code */;    

function resize(e){
    // get width/height with container selector (body also works)
    // or use other method of calculating desired values
    var width = $('#myselector').width(); 
    var height = $('#myselector').height(); 

    // set attrs and 'resume' force 
    svg.attr('width', width);
    svg.attr('height', height);
    force.size([width, height]).resume();
}

W ten sposób nie renderujesz całkowicie wykresu, ustawiamy atrybuty i d3 ponownie oblicza rzeczy w razie potrzeby. Działa to przynajmniej wtedy, gdy używasz punktu ciężkości. Nie jestem pewien, czy jest to warunek wstępny tego rozwiązania. Czy ktoś może potwierdzić lub zaprzeczyć?

Pozdrawiam, g

gavs
źródło
Działa to doskonale na moim układzie siły, który ma około 100 połączeń. Dzięki.
tribe84,
1

Dla tych, którzy używają wykresów skierowanych na siłę w D3 v4 / v5, sizemetoda już nie istnieje. Działało dla mnie coś takiego: (w oparciu o ten problem z github ):

simulation
    .force("center", d3.forceCenter(width / 2, height / 2))
    .force("x", d3.forceX(width / 2))
    .force("y", d3.forceY(height / 2))
    .alpha(0.1).restart();
Justin Lewis
źródło
1

Jeśli chcesz powiązać niestandardową logikę w celu zmiany rozmiaru zdarzenia, w dzisiejszych czasach możesz zacząć używać interfejsu API przeglądarki ResizeObserver do ramki granicznej SVGElement.
Będzie to również obsługiwać przypadek, gdy rozmiar kontenera zostanie zmieniony z powodu zmiany rozmiaru pobliskich elementów.
Istnieje polifill dla szerszej obsługi przeglądarki.

Oto jak to może działać w komponencie interfejsu użytkownika:

function redrawGraph(container, { width, height }) {
  d3
    .select(container)
    .select('svg')
    .attr('height', height)
    .attr('width', width)
    .select('rect')
    .attr('height', height)
    .attr('width', width);
}

// Setup observer in constructor
const resizeObserver = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    // on resize logic specific to this component
    redrawGraph(entry.target, entry.contentRect);
  }
})

// Observe the container
const container = document.querySelector('.graph-container');
resizeObserver.observe(container)
.graph-container {
  height: 75vh;
  width: 75vw;
}

.graph-container svg rect {
  fill: gold;
  stroke: steelblue;
  stroke-width: 3px;
}
<script src="https://unpkg.com/[email protected]/dist/ResizeObserver.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<figure class="graph-container">
  <svg width="100" height="100">
    <rect x="0" y="0" width="100" height="100" />
  </svg>
</figure>

// unobserve in component destroy method
this.resizeObserver.disconnect()
Anbu Agarwal
źródło