Apa cara terbaik untuk membuat tata letak visualisasi d3.js responsif?

224

Asumsikan saya memiliki skrip histogram yang membuat grafik 960 500 svg. Bagaimana cara membuat ini responsif sehingga mengubah ukuran lebar grafik dan ketinggian dinamis?

<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> 

Contoh lengkap histogram gist adalah: https://gist.github.com/993912

Matt Alcock
sumber
5
Saya menemukan metode dalam jawaban ini menjadi lebih mudah: stackoverflow.com/a/11948988/511203
Mike Gorski
cara paling sederhana yang saya temukan adalah membuat lebar dan tinggi svg: 100% dan menerapkan ukuran pada wadah DOM (seperti div) stackoverflow.com/questions/47076163/…
mtx

Jawaban:

316

Ada cara lain untuk melakukan ini yang tidak perlu menggambar ulang grafik, dan itu melibatkan memodifikasi kotak pandang dan mempertahankan atribut AtspectRatio pada <svg>elemen:

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

Pembaruan 11/24/15 : sebagian besar browser modern dapat menyimpulkan rasio aspek elemen SVG dari viewBox, jadi Anda mungkin tidak perlu menjaga ukuran bagan up to date. Jika Anda perlu mendukung peramban yang lebih lama, Anda dapat mengubah ukuran elemen Anda ketika ukuran jendela seperti ini:

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

Dan konten svg akan diskalakan secara otomatis. Anda dapat melihat contoh kerja ini (dengan beberapa modifikasi) di sini : cukup mengubah ukuran jendela atau panel kanan bawah untuk melihat bagaimana reaksinya.

Shawn Allen
sumber
1
Saya sangat suka pendekatan ini shawn, 2 pertanyaan. 1 apakah browser kotak lintas berfungsi? 2. Dalam contoh Anda, ukuran lingkaran tetapi tidak cocok di jendela. Apakah ada yang salah dengan kotak tampilan svg atau aspek rasio.
Matt Alcock
4
Sukai pendekatan ini, tidak yakin cara di atas berfungsi dengan benar. Seperti yang Matt katakan, lingkaran kembali ukuran tetapi jarak dari kiri grafik ke lingkaran pertama tidak mengubah ukuran pada kecepatan yang sama dengan lingkaran. Saya mencoba mengutak-atik jsfiddle, tetapi tidak bisa berfungsi seperti yang diharapkan menggunakan Google Chrome. Browser apa yang kompatibel dengan viewBox dan preserveAspectRatio?
Chris Withers
9
Sebenarnya pengaturan hanya viewBox dan preserveAspectRatio ke SVG dengan tinggi lebar diatur ke 100% dari orang tua, dan orang tua yang menjadi bootstrap / flex grid bekerja untuk saya tanpa menangani acara pengubahan ukuran
Deepu
2
Mengkonfirmasi bahwa Deepu benar. Bekerja dengan Bootstrap / grid fleksibel. Terima kasih @Deepu.
Mike O'Connor
3
Catatan: dalam beberapa tutorial d3 1.) di html lebar dan tinggi svg diatur dan 2.) visualisasi dilakukan melalui onload pemanggilan fungsi. Jika lebar dan tinggi svg diatur dalam html aktual dan Anda menggunakan teknik di atas, gambar tidak akan terukur.
SumNeuron
34

Cari 'SVG responsif' sangat mudah untuk membuat SVG responsif dan Anda tidak perlu khawatir tentang ukuran lagi.

Inilah cara saya melakukannya:

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

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

Info lebih lanjut / tutorial:

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

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

cminatti
sumber
1
Dalam kasus saya, padding-bottom: 100% akan membuat wadah saya memiliki padding tambahan di bagian bawah, maka saya mengubahnya menjadi 50% untuk menghapusnya. Menggunakan kode Anda dan berhasil membuat svg responsif, terima kasih.
V-SHY
20

Saya sudah membuat kode inti kecil untuk menyelesaikan ini.

Pola solusi umum adalah ini:

  1. Breakout script menjadi fungsi komputasi dan menggambar.
  2. Pastikan fungsi gambar menarik secara dinamis dan didorong oleh variabel lebar dan tinggi visualisasi (Cara terbaik untuk melakukannya adalah dengan menggunakan api skala d3.scale)
  3. Bind / chain gambar ke elemen referensi di markup. (Saya menggunakan jquery untuk ini, jadi mengimpornya).
  4. Ingatlah untuk menghapusnya jika sudah ditarik. Dapatkan dimensi dari elemen yang direferensikan menggunakan jquery.
  5. Bind / chain fungsi draw ke fungsi resize window. Kenalkan debounce (batas waktu) ke rantai ini untuk memastikan kami hanya menggambar ulang setelah batas waktu.

Saya juga menambahkan skrip d3.js yang diperkecil untuk kecepatan. Intinya ada di sini: https://gist.github.com/2414111

referensi kembali kode jquery:

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

Kode debounce:

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

Nikmati!

Matt Alcock
sumber
Tampaknya ini tidak berfungsi; jika saya mengubah ukuran jendela file html yang termasuk dalam inti yang dikoreksi, histogram masih akan terpotong ketika jendela menjadi lebih kecil ...
Chris Withers
1
SVG adalah format vektor. Anda dapat mengatur ukuran kotak tampilan virtual dan kemudian skala ke dimensi nyata. Tidak perlu menggunakan JS.
phil pirozhkov
4

Tanpa Menggunakan ViewBox

Berikut adalah contoh solusi yang tidak bergantung pada penggunaan viewBox:

Kuncinya adalah dalam memperbarui kisaran dari skala yang digunakan untuk menempatkan data.

Pertama, hitung rasio aspek asli Anda:

var ratio = width / height;

Kemudian, pada setiap mengubah ukuran, memperbarui rangedari xdan y:

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

Perhatikan bahwa ketinggian didasarkan pada lebar dan rasio aspek, sehingga proporsi asli Anda dipertahankan.

Terakhir, "gambar ulang" grafik - perbarui atribut yang bergantung pada salah satu xatau yskala:

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

Perhatikan bahwa dalam mengubah ukuran rectsAnda dapat menggunakan batas atas rangedari y, daripada secara eksplisit menggunakan ketinggian:

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

Paul Murray
sumber
3

Jika Anda menggunakan d3.js hingga c3.js solusi untuk masalah respons cukup mudah:

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

di mana HTML yang dihasilkan terlihat seperti:

<div id="chart">
    <svg>...</svg>
</div>
vanna
sumber
2

Jawaban Shawn Allen luar biasa. Tetapi Anda mungkin tidak ingin melakukan ini setiap saat. Jika Anda meng-host-nya di vida.io , Anda mendapatkan responsif otomatis untuk visualisasi svg Anda.

Anda bisa mendapatkan iframe responsif dengan kode sematan sederhana ini:

<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/

Pengungkapan: saya membangun fitur ini di vida.io .

Phuoc Do
sumber
2

Jika Anda menggunakan pembungkus d3 seperti plottable.js , perlu diketahui bahwa solusi termudah mungkin menambahkan pendengar acara dan kemudian memanggil fungsi redraw ( redrawdalam plottable.js). Dalam kasus plottable.js ini akan berfungsi dengan baik (pendekatan ini tidak terdokumentasi dengan baik):

    window.addEventListener("resize", function() {
      table.redraw();
    });
Riley Steele Parsons
sumber
Mungkin akan lebih baik untuk menghilangkan fungsi ini untuk menghindari menembakkan redraw secepat mungkin
Ian
1

Seandainya orang masih mengunjungi pertanyaan ini - inilah yang bekerja untuk saya:

  • Lampirkan iframe di div dan gunakan css untuk menambahkan padding, katakanlah, 40% ke div itu (persentase tergantung pada rasio aspek yang Anda inginkan). Kemudian atur lebar dan tinggi iframe itu sendiri menjadi 100%.

  • Dalam dokumen html yang berisi bagan yang akan dimuat dalam iframe, atur lebar ke lebar div yang ditambahkan ke svg (atau ke lebar badan) dan setel rasio aspek tinggi ke lebar *.

  • Tulis fungsi yang memuat ulang konten iframe pada ukuran jendela, untuk menyesuaikan ukuran grafik ketika orang memutar ponsel mereka.

Contoh di sini di situs web saya: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

UPDATE 30 Des 2016

Pendekatan yang saya jelaskan di atas memiliki beberapa kelemahan, terutama karena tidak memperhitungkan ketinggian judul dan keterangan yang bukan bagian dari svg yang dibuat D3. Sejak itu saya menemukan apa yang saya pikir merupakan pendekatan yang lebih baik:

  • Atur lebar bagan D3 ke lebar div yang dilampirkan dan gunakan rasio aspek untuk mengatur ketinggiannya sesuai;
  • Minta halaman yang disematkan mengirim tinggi dan url ke halaman induk menggunakan postMessage HTML5;
  • Pada halaman induk, gunakan url untuk mengidentifikasi iframe yang sesuai (berguna jika Anda memiliki lebih dari satu iframe di halaman Anda) dan perbarui ketinggiannya ke ketinggian halaman yang disematkan.

Contoh di sini di situs web saya: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsif-website-better-solution

dirkmjk
sumber
0

Salah satu prinsip dasar dari penggabungan data D3 adalah idempoten. Dengan kata lain, jika Anda berulang kali mengevaluasi data-gabung dengan data yang sama, output yang diberikan sama. Oleh karena itu, selama Anda membuat bagan Anda dengan benar, berhati-hatilah dengan pilihan masuk, perbarui dan keluar - yang harus Anda lakukan ketika ukurannya berubah, adalah render ulang bagan itu secara keseluruhan.

Ada beberapa hal lain yang harus Anda lakukan, salah satunya adalah de-bouncing jendela handler ukuran untuk mencekiknya. Selain itu, daripada lebar / ketinggian hard-coding, ini harus dicapai dengan mengukur elemen yang mengandung.

Sebagai alternatif, berikut adalah bagan Anda yang dirender menggunakan d3fc , yang merupakan sekumpulan komponen D3 yang secara benar menangani penggabungan data. Ia juga memiliki bagan kartesian yang mengukurnya mengandung elemen yang membuatnya mudah untuk membuat bagan 'responsif':

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

Anda dapat melihatnya beraksi di codepen ini:

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

di mana Anda dapat mengubah ukuran jendela dan memverifikasi bahwa bagan dibuat ulang dengan benar.

Harap dicatat, sebagai pengungkapan penuh, saya adalah salah satu pengelola d3fc.

ColinE
sumber
Terima kasih telah memperbarui jawaban Anda! Saya telah melihat pembaruan terkini untuk beberapa posting Anda, dan saya sangat menghargai orang-orang yang mengunjungi kembali konten mereka sendiri! Namun, sebagai peringatan, revisi ini tidak sepenuhnya sesuai dengan pertanyaan lagi, karena Anda mengedit di D3 v4 , sedangkan OP jelas merujuk ke v3 , yang mungkin membingungkan pembaca di masa depan. Secara pribadi, untuk jawaban saya sendiri, saya cenderung menambahkan bagian v4 sambil tetap mempertahankan bagian v3 asli . Saya pikir v4 mungkin layak jawabannya sendiri menambahkan referensi timbal balik untuk kedua posting. Tapi itu hanya dua sen saya ...
altocumulus
Itu poin yang sangat bagus! Sangat mudah untuk hanyut dan hanya fokus pada masa depan, dan apa yang baru. Dilihat oleh pertanyaan d3 terbaru saya pikir banyak orang masih menggunakan v3. Ini juga membuat frustasi betapa halus perbedaannya! Saya akan meninjau kembali hasil edit saya :-)
ColinE
0

Saya akan menghindari pengubahan ukuran / centang solusi seperti wabah karena tidak efisien dan dapat menyebabkan masalah pada aplikasi Anda (mis. Tooltip menghitung ulang posisi yang seharusnya muncul pada ukuran jendela, lalu sesaat kemudian grafik Anda juga mengubah ukuran dan halaman kembali tata letak dan sekarang tooltip Anda salah lagi).

Anda dapat mensimulasikan perilaku ini di beberapa browser lama yang tidak mendukung dengan benar seperti IE11 juga menggunakan <canvas>elemen yang mempertahankan aspek itu.

Diberikan 960x540 yang merupakan aspek 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>
Dominic
sumber
0

Banyak jawaban kompleks di sini.

Pada dasarnya yang perlu Anda lakukan adalah parit widthdan heightatribut yang mendukung viewBoxatribut:

width = 500;
height = 500;

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

Jika Anda memiliki margin, Anda bisa menambahkannya di sana ke dalam lebar / tinggi kemudian tambahkan saja gdan ubahlah seperti biasa.

Jared Wilber
sumber
-1

Anda juga dapat menggunakan bootstrap 3 untuk menyesuaikan ukuran visualisasi. Misalnya, kita dapat mengatur kode HTML sebagai:

<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>

Saya telah mengatur ketinggian tetap karena kebutuhan saya, tetapi Anda dapat membiarkan ukuran otomatis juga. "Col-sm-6 col-md-4" membuat div responsif untuk perangkat yang berbeda. Anda dapat mempelajari lebih lanjut di http://getbootstrap.com/css/#grid-example-basic

Kami dapat mengakses grafik dengan bantuan id bulan-tampilan .

Saya tidak akan membahas banyak tentang kode d3, saya hanya akan memasukkan bagian yang diperlukan untuk beradaptasi dengan ukuran layar yang berbeda.

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

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

Lebar diatur dengan mendapatkan lebar div dengan tampilan bulan id.

Ketinggian dalam kasus saya tidak harus mencakup seluruh area. Saya juga memiliki beberapa teks di atas bilah jadi saya perlu menghitung area itu juga. Itu sebabnya saya mengidentifikasi area teks dengan id responsesivetext. Untuk menghitung ketinggian bar yang diizinkan, saya mengurangi ketinggian teks dari ketinggian div.

Ini memungkinkan Anda memiliki bilah yang akan mengadopsi semua ukuran layar / div yang berbeda. Ini mungkin bukan cara terbaik untuk melakukannya, tetapi pasti bekerja untuk kebutuhan proyek saya.

arlindmusliu
sumber
1
Hai, saya mencoba ini, tetapi konten svg tidak mengubah ukuran, seperti kalender atau bagan.