Jaki jest najlepszy sposób, aby układ wizualizacji d3.js był responsywny?

224

Załóżmy, że mam skrypt histogramu, który buduje grafikę 960 500 svg. Jak sprawić, by to reagowało, więc zmieniając szerokość, szerokość i wysokość grafiki były dynamiczne?

<script> 

var n = 10000, // number of trials
    m = 10,    // number of random variables
    data = [];

// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
  for (var s = 0, j = 0; j < m; j++) {
    s += Math.random();
  }
  data.push(s);
}

var histogram = d3.layout.histogram()
    (data);

var width = 960,
    height = 500;

var x = d3.scale.ordinal()
    .domain(histogram.map(function(d) { return d.x; }))
    .rangeRoundBands([0, width]);

var y = d3.scale.linear()
    .domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
    .range([0, height]);

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

svg.selectAll("rect")
    .data(histogram)
  .enter().append("rect")
    .attr("width", x.rangeBand())
    .attr("x", function(d) { return x(d.x); })
    .attr("y", function(d) { return height - y(d.y); })
    .attr("height", function(d) { return y(d.y); });

svg.append("line")
    .attr("x1", 0)
    .attr("x2", width)
    .attr("y1", height)
    .attr("y2", height);

</script> 

Pełny przykładowy wykres histogramu to: https://gist.github.com/993912

Matt Alcock
źródło
5
Uznałem, że metoda w tej odpowiedzi jest łatwiejsza: stackoverflow.com/a/11948988/511203
Mike Gorski
najprostszym sposobem, jaki znalazłem, jest zrobienie szerokości i wysokości svg: 100% i zastosowanie rozmiaru kontenera DOM (jak div) stackoverflow.com/questions/47076163/...
mtx

Odpowiedzi:

316

Jest inny sposób na to, który nie wymaga przerysowywania wykresu i polega na modyfikacji atrybutów viewBox i preserveAspectRatio na <svg>elemencie:

<svg id="chart" width="960" height="500"
  viewBox="0 0 960 500"
  preserveAspectRatio="xMidYMid meet">
</svg>

Aktualizacja 24.11.2015 : większość współczesnych przeglądarek może wywnioskować proporcje elementów SVG na podstawie viewBox, więc może nie być konieczne aktualizowanie rozmiaru wykresu. Jeśli potrzebujesz obsługi starszych przeglądarek, możesz zmienić rozmiar swojego elementu, gdy rozmiar okna się zmieni:

var aspect = width / height,
    chart = d3.select('#chart');
d3.select(window)
  .on("resize", function() {
    var targetWidth = chart.node().getBoundingClientRect().width;
    chart.attr("width", targetWidth);
    chart.attr("height", targetWidth / aspect);
  });

Zawartość SVG zostanie automatycznie przeskalowana. Możesz zobaczyć działający przykład tego (z pewnymi modyfikacjami) tutaj : wystarczy zmienić rozmiar okna lub prawy dolny panel, aby zobaczyć, jak zareaguje.

Shawn Allen
źródło
1
Bardzo podoba mi się to podejście shawn, 2 pytania. 1 czy viewbox działa w różnych przeglądarkach? 2. W twoim przykładzie koła zmieniają rozmiar, ale nie mieszczą się w oknie. Czy coś idzie nie tak z polem widzenia SVG lub współczynnikiem proporcji.
Matt Alcock
4
Uwielbiam to podejście, nie jestem pewien, czy powyższe działa poprawnie. Jak mówi Matt, koła zmieniają rozmiar, ale odległość od lewej strony wykresu do pierwszego koła nie zmienia rozmiaru w takim samym tempie, jak koła. Próbowałem trochę bawić się w jsfiddle, ale nie mogłem go uruchomić zgodnie z oczekiwaniami przy użyciu Google Chrome. Jakie przeglądarki są kompatybilne z viewBox i preserveAspectRatio?
Chris Withers
9
Właściwie ustawienie tylko viewBox i preserveAspectRatio na SVG z szerokością ustawioną na 100% rodzica, a rodzic będący bootstrap / flex grid działa dla mnie bez obsługi zdarzenia zmiany rozmiaru
Deepu
2
Potwierdzenie, że Deepu jest poprawne. Współpracuje z siatką Bootstrap / Flex. Dzięki @Deepu.
Mike O'Connor,
3
Uwaga: w kilku samouczkach d3 1.) w html są ustawione szerokość i wysokość svg oraz 2.) wizualizacja odbywa się za pomocą onload wywołania funkcji. Jeśli szerokość i wysokość svg są ustawione w rzeczywistym html i użyjesz powyższej techniki, obraz nie będzie skalowalny.
SumNeuron
34

Poszukaj „responsive SVG”. Bardzo łatwo jest stworzyć responsive SVG i nie musisz się już martwić o rozmiary.

Oto jak to zrobiłem:

d3.select("div#chartId")
   .append("div")
   .classed("svg-container", true) //container class to make it responsive
   .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); 

Kod CSS:

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

Więcej informacji / tutoriale:

http://demosthenes.info/blog/744/Make-SVG-Responsive

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

cminatti
źródło
1
W moim przypadku wypełnienie dołu: 100% sprawi, że mój pojemnik będzie miał dodatkowe wypełnienie na dole, dlatego zmieniłem go na 50%, aby go usunąć. Wykorzystałeś swój kod i z powodzeniem sprawiłeś, że svg szybko zareagował, dzięki.
V-SHY,
20

Napisałem małą istotę, aby rozwiązać ten problem.

Ogólny wzorzec rozwiązania jest następujący:

  1. Podziel skrypt na funkcje obliczeniowe i rysunkowe.
  2. Upewnij się, że funkcja rysowania rysuje dynamicznie i jest sterowana zmiennymi szerokości i wysokości wizualizacji (Najlepszym sposobem na to jest użycie interfejsu API d3.scale)
  3. Powiąż / połącz rysunek z elementem odniesienia w znacznikach. (Użyłem do tego jquery, więc zaimportowałem ją).
  4. Pamiętaj, aby go usunąć, jeśli jest już narysowany. Uzyskaj wymiary z odnośnego elementu za pomocą jquery.
  5. Powiąż / połącz funkcję rysowania z funkcją zmiany rozmiaru okna. Wprowadź do tego łańcucha debounce (limit czasu), aby przerysować tylko po upływie limitu czasu.

Dodałem również zminimalizowany skrypt d3.js dla prędkości. Istota jest tutaj: https://gist.github.com/2414111

kod zwrotny referencji jquery:

$(reference).empty()
var width = $(reference).width();

Kod odrzucenia:

var debounce = function(fn, timeout) 
{
  var timeoutID = -1;
  return function() {
     if (timeoutID > -1) {
        window.clearTimeout(timeoutID);
     }
   timeoutID = window.setTimeout(fn, timeout);
  }
};

var debounced_draw = debounce(function() {
    draw_histogram(div_name, pos_data, neg_data);
  }, 125);

 $(window).resize(debounced_draw);

Cieszyć się!

Matt Alcock
źródło
To nie wydaje się działać; jeśli zmienię rozmiar okna pliku HTML zawartego w poprawionej liście, histogram nadal zostanie odcięty, gdy okno się zmniejszy ...
Chris Withers,
1
SVG to format wektorowy. Możesz ustawić dowolny wirtualny rozmiar viewboxa, a następnie przeskalować go do rzeczywistych wymiarów. Nie musisz używać JS.
phil pirozhkov
4

Bez użycia ViewBox

Oto przykład rozwiązania, które nie polega na użyciu viewBox:

Klucz jest w aktualizowanie zakres z łusek , które są wykorzystywane do umieszczania danych.

Najpierw obliczyć oryginalny współczynnik kształtu:

var ratio = width / height;

Następnie na każdej rozmiaru, zaktualizuj rangeod xi y:

function resize() {
  x.rangeRoundBands([0, window.innerWidth]);
  y.range([0, window.innerWidth / ratio]);
  svg.attr("height", window.innerHeight);
}

Należy pamiętać, że wysokość zależy od szerokości i współczynnika kształtu, dzięki czemu zachowane zostaną oryginalne proporcje.

Na koniec „przerysuj” wykres - zaktualizuj dowolny atrybut, który zależy od jednej z xlub yskal:

function redraw() {
    rects.attr("width", x.rangeBand())
      .attr("x", function(d) { return x(d.x); })
      .attr("y", function(d) { return y.range()[1] - y(d.y); })
      .attr("height", function(d) { return y(d.y); });
}

Należy zauważyć, że w re-doborze rectsmożna użyć górny związany z rangeo y, zamiast jawnie przy użyciu wysokość:

.attr("y", function(d) { return y.range()[1] - y(d.y); })

Paul Murray
źródło
3

Jeśli używasz d3.js do c3.js, rozwiązanie problemu z odpowiedzią jest dość proste:

var chart = c3.generate({bindTo:"#chart",...});
chart.resize($("#chart").width(),$("#chart").height());

gdzie wygląda wygenerowany kod HTML:

<div id="chart">
    <svg>...</svg>
</div>
Vanna
źródło
2

Odpowiedź Shawna Allena była świetna. Ale możesz nie chcieć tego robić za każdym razem. Jeśli hostujesz go na vida.io , automatycznie reagujesz na wizualizację SVG .

Za pomocą tego prostego kodu do umieszczenia można uzyskać responsywny element iframe:

<div id="vida-embed">
<iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
</div>

#vida-embed iframe {
  position: absolute;
  top:0;
  left: 0;
  width: 100%;
  height: 100%;
}

http://jsfiddle.net/dnprock/npxp3v9d/1/

Ujawnienie: buduję tę funkcję w vida.io .

Phuoc Do
źródło
2

W przypadku korzystania z opakowania d3, takiego jak plottable.js , należy pamiętać, że najłatwiejszym rozwiązaniem może być dodanie detektora zdarzeń, a następnie wywołanie funkcji przerysowania ( redraww plottable.js). W przypadku plottable.js zadziała to doskonale (takie podejście jest słabo udokumentowane):

    window.addEventListener("resize", function() {
      table.redraw();
    });
Riley Steele Parsons
źródło
Prawdopodobnie najlepiej byłoby ogłosić tę funkcję, aby uniknąć jak najszybszego uruchomienia przerysowania
Ian
1

W przypadku, gdy ludzie nadal odwiedzają to pytanie - oto, co zadziałało dla mnie:

  • Zawiąż ramkę iframe w div i użyj css, aby dodać dopełnienie, powiedzmy, 40% do tego div (procent w zależności od pożądanego współczynnika proporcji). Następnie ustaw zarówno szerokość, jak i wysokość samego elementu iframe na 100%.

  • W dokumencie HTML zawierającym wykres, który ma zostać załadowany do ramki iframe, ustaw szerokość na szerokość div, do której dołączany jest plik svg (lub na szerokość ciała) i ustaw proporcje wysokości do szerokości *.

  • Napisz funkcję, która ponownie ładuje zawartość iframe po zmianie rozmiaru okna, aby dostosować rozmiar wykresu, gdy ludzie obracają telefon.

Przykład tutaj na mojej stronie: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

AKTUALIZACJA 30 grudnia 2016 r

Podejście, które opisałem powyżej, ma pewne wady, zwłaszcza że nie bierze pod uwagę wysokości żadnego tytułu i podpisów, które nie są częścią svg utworzonego w D3. Od tamtej pory natknąłem się na lepsze podejście:

  • Ustaw szerokość wykresu D3 na szerokość elementu div, do którego jest dołączony, i użyj współczynnika proporcji, aby odpowiednio ustawić jego wysokość;
  • Niech osadzona strona wyśle ​​swoją wysokość i adres URL na stronę nadrzędną za pomocą postMessage HTML5;
  • Na stronie nadrzędnej użyj adresu URL, aby zidentyfikować odpowiedni element iframe (przydatny, jeśli masz więcej niż jeden element iframe na stronie) i zaktualizuj jego wysokość do wysokości osadzonej strony.

Przykład tutaj na mojej stronie internetowej: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution

dirkmjk
źródło
0

Jedną z podstawowych zasad łączenia danych D3 jest to, że jest idempotentny. Innymi słowy, jeśli wielokrotnie oceniasz połączenie danych z tymi samymi danymi, renderowany wynik jest taki sam. Dlatego tak długo, jak poprawnie wyrenderujesz wykres, zachowując ostrożność przy wprowadzaniu, aktualizacji i wychodzeniu - wszystko, co musisz zrobić, gdy zmienia się rozmiar, to ponownie renderuj wykres w całości.

Jest kilka innych rzeczy, które powinieneś zrobić, jedną z nich jest odskakiwanie modułu zmiany rozmiaru okna w celu dławienia go. Ponadto zamiast sztywnych szerokości / wysokości należy to osiągnąć poprzez pomiar elementu zawierającego.

Alternatywnie, oto twój wykres renderowany przy użyciu d3fc , który jest zestawem składników D3, które poprawnie obsługują łączenia danych. Ma także kartezjańską tabelę, która mierzy ją, zawierając element, co ułatwia tworzenie „responsywnych” wykresów:

// create some test data
var data = d3.range(50).map(function(d) {
  return {
    x: d / 4,
    y: Math.sin(d / 4),
    z: Math.cos(d / 4) * 0.7
  };
});

var yExtent = fc.extentLinear()
  .accessors([
    function(d) { return d.y; },
    function(d) { return d.z; }
  ])
  .pad([0.4, 0.4])
  .padUnit('domain');

var xExtent = fc.extentLinear()
  .accessors([function(d) { return d.x; }]);

// create a chart
var chart = fc.chartSvgCartesian(
    d3.scaleLinear(),
    d3.scaleLinear())
  .yDomain(yExtent(data))
  .yLabel('Sine / Cosine')
  .yOrient('left')
  .xDomain(xExtent(data))
  .xLabel('Value')
  .chartLabel('Sine/Cosine Line/Area Chart');

// create a pair of series and some gridlines
var sinLine = fc.seriesSvgLine()
  .crossValue(function(d) { return d.x; })
  .mainValue(function(d) { return d.y; })
  .decorate(function(selection) {
    selection.enter()
      .style('stroke', 'purple');
  });

var cosLine = fc.seriesSvgArea()
  .crossValue(function(d) { return d.x; })
  .mainValue(function(d) { return d.z; })
  .decorate(function(selection) {
    selection.enter()
      .style('fill', 'lightgreen')
      .style('fill-opacity', 0.5);
  });

var gridlines = fc.annotationSvgGridline();

// combine using a multi-series
var multi = fc.seriesSvgMulti()
  .series([gridlines, sinLine, cosLine]);

chart.plotArea(multi);

// render
d3.select('#simple-chart')
  .datum(data)
  .call(chart);

Możesz zobaczyć to w akcji w tym codepen:

https://codepen.io/ColinEberhardt/pen/dOBvOy

gdzie możesz zmienić rozmiar okna i sprawdzić, czy wykres jest poprawnie ponownie renderowany.

Pamiętaj, że jako pełne ujawnienie jestem jednym z opiekunów d3fc.

ColinE
źródło
Dziękujemy za aktualizację twoich odpowiedzi! Widziałem ostatnie aktualizacje niektórych z twoich postów i naprawdę doceniam ludzi, którzy wracają do swoich treści! Jednak, jako ostrzeżenie, ta wersja nie pasuje już do pytania, ponieważ edytujesz w D3 v4 , podczas gdy OP wyraźnie odnosi się do v3 , co może dezorientować przyszłych czytelników. Osobiście, dla moich własnych odpowiedzi, zwykle dodam sekcję v4, jednocześnie zachowując oryginalną sekcję v3 . Myślę, że v4 może być warte własnej odpowiedzi, dodając wzajemne odniesienie do obu postów. Ale to tylko moje dwa centy ...
altocumulus
To naprawdę dobry punkt! Łatwo jest dać się ponieść emocjom i skupić się na przyszłości i nowościach. Sądząc po najnowszych pytaniach d3, myślę, że wiele osób nadal używa v3. Frustrujące jest także to, jak subtelne są niektóre różnice!
Przejdę
0

Unikałbym zmiany rozmiaru / zaznaczenia rozwiązań, takich jak plaga, ponieważ są one nieefektywne i mogą powodować problemy w Twojej aplikacji (np. Etykietka ponownie oblicza pozycję, w jakiej powinna się wyświetlać przy zmianie rozmiaru okna, a chwilę później zmienia się również rozmiar wykresu i strona się zmienia układy, a teraz ponownie podpowiedź).

Możesz symulować to zachowanie w niektórych starszych przeglądarkach, które nie obsługują go poprawnie, np. IE11, również za pomocą <canvas> elementu, który zachowuje jego aspekt.

Biorąc pod uwagę 960 x 540, który jest aspektem 16: 9:

<div style="position: relative">
  <canvas width="16" height="9" style="width: 100%"></canvas>
  <svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
  </svg>
</div>
Dominik
źródło
0

Wiele skomplikowanych odpowiedzi tutaj.

Zasadniczo wszystko, co musisz zrobić, to porzucić atrybuty widthi heightna rzecz viewBoxatrybutu:

width = 500;
height = 500;

const svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

Jeśli masz marginesy, możesz po prostu dodać je tam do szerokości / wysokości, a następnie po prostu dołączyć je gpóźniej i przekształcić tak, jak zwykle.

Jared Wilber
źródło
-1

Możesz także użyć bootstrap 3, aby dostosować rozmiar wizualizacji. Na przykład możemy ustawić kod HTML jako:

<div class="container>
<div class="row">

<div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
<div id ="responsivetext">Something to write</div>
</div>

</div>
</div>

Ustawiłem stałą wysokość ze względu na moje potrzeby, ale możesz również pozostawić automatyczny rozmiar. „Col-sm-6 col-md-4” sprawia, że ​​div reaguje na różne urządzenia. Możesz dowiedzieć się więcej na http://getbootstrap.com/css/#grid-example-basic

Możemy uzyskać dostęp do wykresu za pomocą identyfikatora miesięcznego widoku .

Nie będę szczegółowo omawiał kodu d3, wprowadzę tylko część potrzebną do dostosowania do różnych rozmiarów ekranu.

var width = document.getElementById('month-view').offsetWidth;

var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;

Szerokość jest ustawiana przez uzyskanie szerokości div z identyfikatorem widok miesiąca.

Wysokość w moim przypadku nie powinna obejmować całego obszaru. Mam też tekst nad paskiem, więc muszę też obliczyć ten obszar. Dlatego zidentyfikowałem obszar tekstu za pomocą id responsivetext. Aby obliczyć dozwoloną wysokość paska, odjąłem wysokość tekstu od wysokości div.

To pozwala ci mieć pasek, który przyjmie wszystkie różne rozmiary ekranu / div. Może nie jest to najlepszy sposób na zrobienie tego, ale z pewnością działa na potrzeby mojego projektu.

arlindmusliu
źródło
1
Cześć, próbowałem tego, ale zawartość svg nie zmienia rozmiaru, podobnie jak kalendarz lub wykresy.