Menuju Grafik D3.js yang Dapat Diperbarui
Diterbitkan: 2022-03-11pengantar
D3.js adalah perpustakaan sumber terbuka untuk visualisasi data yang dikembangkan oleh Mike Bostock. D3 adalah singkatan dari dokumen berbasis data, dan seperti namanya, perpustakaan memungkinkan pengembang untuk dengan mudah menghasilkan dan memanipulasi elemen DOM berdasarkan data. Meskipun tidak dibatasi oleh kemampuan perpustakaan, D3.js biasanya digunakan dengan elemen SVG dan menawarkan alat yang ampuh untuk mengembangkan visualisasi data vektor dari awal.
Mari kita mulai dengan contoh sederhana. Misalkan Anda berlatih untuk perlombaan 5k, dan Anda ingin membuat diagram batang horizontal dari jumlah mil yang telah Anda jalankan setiap hari dalam seminggu terakhir:
var milesRun = [2, 5, 4, 1, 2, 6, 5]; d3.select('body').append('svg') .attr('height', 300) .attr('width', 800) .selectAll('rect') .data(milesRun) .enter() .append('rect') .attr('y', function (d, i) { return i * 40 }) .attr('height', 35) .attr('x', 0) .attr('width', function (d) { return d*100}) .style('fill', 'steelblue');
Untuk melihatnya beraksi, lihat di bl.ocks.org.
Jika kode ini terlihat familier, itu bagus. Jika tidak, saya menemukan tutorial Scott Murray sebagai sumber yang bagus untuk memulai dengan D3.js.
Sebagai pekerja lepas yang telah bekerja ratusan jam mengembangkan dengan D3.js, pola pengembangan saya telah melalui evolusi, selalu dengan tujuan akhir untuk menciptakan pengalaman klien dan pengguna yang paling komprehensif. Seperti yang akan saya bahas secara lebih rinci nanti, pola Mike Bostock untuk bagan yang dapat digunakan kembali menawarkan metode yang telah dicoba dan benar untuk menerapkan bagan yang sama dalam sejumlah pilihan. Namun, keterbatasannya terwujud setelah grafik diinisialisasi. Jika saya ingin menggunakan transisi dan pola pembaruan D3 dengan metode ini, perubahan pada data harus ditangani sepenuhnya dalam lingkup yang sama dengan bagan yang dihasilkan. Dalam praktiknya, ini berarti menerapkan filter, pilihan dropdown, penggeser, dan opsi pengubahan ukuran semua dalam lingkup fungsi yang sama.
Setelah berulang kali mengalami keterbatasan ini secara langsung, saya ingin menciptakan cara untuk memanfaatkan kekuatan penuh D3.js. Misalnya, mendengarkan perubahan pada dropdown dari komponen yang benar-benar terpisah dan dengan mulus memicu pembaruan bagan dari data lama ke data baru. Saya ingin dapat menyerahkan kontrol bagan dengan fungsionalitas penuh, dan melakukannya dengan cara yang logis dan modular. Hasilnya adalah pola grafik yang dapat diperbarui, dan saya akan membahas perkembangan lengkap saya untuk membuat pola ini.
Perkembangan Pola Grafik D3.js
Langkah 1: Variabel Konfigurasi
Saat saya mulai menggunakan D3.js untuk mengembangkan visualisasi, menjadi sangat nyaman menggunakan variabel konfigurasi untuk mendefinisikan dan mengubah spesifikasi bagan dengan cepat. Ini memungkinkan bagan saya untuk menangani semua panjang dan nilai data yang berbeda. Bagian kode yang sama yang menampilkan jarak tempuh sekarang dapat menampilkan daftar suhu yang lebih panjang tanpa gangguan:
var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68]; var height = 300; var width = 800; var barPadding = 1; var barSpacing = height / highTemperatures.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(highTemperatures); var widthScale = width / maxValue; d3.select('body').append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(highTemperatures) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', 'steelblue');
Untuk melihatnya beraksi, lihat di bl.ocks.org.
Perhatikan bagaimana tinggi dan lebar batang diskalakan berdasarkan ukuran dan nilai data. Satu variabel diubah, dan sisanya diurus.
Langkah 2: Pengulangan Mudah Melalui Fungsi
Dengan mengabstraksi beberapa logika bisnis, kami dapat membuat kode yang lebih serbaguna yang siap menangani template data yang digeneralisasi. Langkah selanjutnya adalah membungkus kode ini menjadi fungsi generasi, yang mengurangi inisialisasi menjadi hanya satu baris. Fungsi ini mengambil tiga argumen: data, target DOM, dan objek opsi yang dapat digunakan untuk menimpa variabel konfigurasi default. Lihatlah bagaimana ini bisa dilakukan:
var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; function drawChart(dom, data, options) { var width = options.width || 800; var height = options.height || 200; var barPadding = options.barPadding || 1; var fillColor = options.fillColor || 'steelblue'; var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(dom).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); } var weatherOptions = {fillColor: 'coral'}; drawChart('#weatherHistory', highTemperatures, weatherOptions); var runningOptions = {barPadding: 2}; drawChart('#runningHistory', milesRun, runningOptions);
Untuk melihatnya beraksi, lihat di bl.ocks.org.
Penting juga untuk membuat catatan tentang pilihan D3.js dalam konteks ini. Pilihan umum seperti d3.selectAll('rect')
harus selalu dihindari. Jika SVG ada di tempat lain di halaman, semua rect
di halaman menjadi bagian dari seleksi. Sebagai gantinya, menggunakan referensi DOM yang diteruskan, buat satu objek svg
yang dapat Anda rujuk saat menambahkan dan memperbarui elemen. Teknik ini juga dapat meningkatkan runtime pembuatan bagan, karena menggunakan referensi seperti batang juga mencegah keharusan membuat pemilihan D3.js lagi.
Langkah 3: Metode Chaining dan Seleksi
Sementara kerangka sebelumnya menggunakan objek konfigurasi sangat umum di seluruh pustaka JavaScript, Mike Bostock, pencipta D3.js, merekomendasikan pola lain untuk membuat bagan yang dapat digunakan kembali. Singkatnya, Mike Bostock merekomendasikan penerapan grafik sebagai penutupan dengan metode pengambil-setter. Sambil menambahkan beberapa kerumitan pada implementasi bagan, pengaturan opsi konfigurasi menjadi sangat mudah bagi pemanggil hanya dengan menggunakan metode chaining:
// Using Mike Bostock's Towards Reusable Charts Pattern function barChart() { // All options that should be accessible to caller var width = 900; var height = 200; var barPadding = 1; var fillColor = 'steelblue'; function chart(selection){ selection.each(function (data) { var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(this).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); }); } chart.width = function(value) { if (!arguments.length) return margin; width = value; return chart; }; chart.height = function(value) { if (!arguments.length) return height; height = value; return chart; }; chart.barPadding = function(value) { if (!arguments.length) return barPadding; barPadding = value; return chart; }; chart.fillColor = function(value) { if (!arguments.length) return fillColor; fillColor = value; return chart; }; return chart; } var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; var runningChart = barChart().barPadding(2); d3.select('#runningHistory') .datum(milesRun) .call(runningChart); var weatherChart = barChart().fillColor('coral'); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);
Untuk melihatnya beraksi, lihat di bl.ocks.org.
Inisialisasi bagan menggunakan pilihan D3.js, mengikat data yang relevan dan meneruskan pemilihan DOM sebagai konteks this
ke fungsi generator. Fungsi generator membungkus variabel default dalam penutupan, dan memungkinkan pemanggil untuk mengubah ini melalui metode chaining dengan fungsi konfigurasi yang mengembalikan objek grafik. Dengan melakukan ini, pemanggil dapat merender bagan yang sama ke beberapa pilihan sekaligus, atau menggunakan satu bagan untuk merender grafik yang sama ke pilihan yang berbeda dengan data yang berbeda, sambil menghindari melewati objek opsi yang besar.

Langkah 4: Pola Baru untuk Bagan yang Dapat Diperbarui
Pola sebelumnya yang disarankan oleh Mike Bostock memberi kita, sebagai pengembang grafik, banyak kekuatan dalam fungsi generator. Dengan satu set data dan konfigurasi berantai yang diteruskan, kami mengontrol semuanya dari sana. Jika data perlu diubah dari dalam, kita dapat menggunakan transisi yang sesuai daripada hanya menggambar ulang dari awal. Bahkan hal-hal seperti pengubahan ukuran jendela dapat ditangani dengan elegan, membuat fitur responsif seperti menggunakan teks yang disingkat atau mengubah label sumbu.
Namun bagaimana jika data tersebut dimodifikasi dari luar lingkup fungsi generator? Atau bagaimana jika bagan perlu diubah ukurannya secara terprogram? Kita bisa memanggil fungsi grafik lagi, dengan data baru dan konfigurasi ukuran baru. Semuanya akan digambar ulang, dan voila. Masalah terpecahkan.
Sayangnya, ada sejumlah masalah dengan solusi ini.
Pertama, kami hampir pasti melakukan perhitungan inisialisasi yang tidak perlu. Mengapa manipulasi data yang rumit ketika yang harus kita lakukan hanyalah menskalakan lebarnya? Perhitungan ini mungkin diperlukan saat pertama kali bagan diinisialisasi, tetapi tentu saja tidak pada setiap pembaruan yang perlu kita buat. Setiap permintaan terprogram memerlukan beberapa modifikasi, dan sebagai pengembang kami tahu persis apa perubahan ini. Tidak lebih, tidak kurang. Selain itu, dalam cakupan bagan, kami telah memiliki akses ke banyak hal yang kami butuhkan (objek SVG, status data saat ini, dan lainnya) membuat perubahan mudah diterapkan.
Ambil contoh contoh diagram batang di atas. Jika kita ingin memperbarui lebar, dan melakukannya dengan menggambar ulang seluruh bagan, kita akan memicu banyak perhitungan yang tidak perlu: menemukan nilai data maksimum, menghitung tinggi batang, dan merender semua elemen SVG ini. Sungguh, setelah width
ditetapkan ke nilai barunya, satu-satunya perubahan yang perlu kita lakukan adalah:
width = newWidth; widthScale = width / maxValue; bars.attr('width', function(d) { return d*widthScale}); svg.attr('width', width);
Tapi itu menjadi lebih baik. Karena sekarang kita memiliki beberapa riwayat bagan, kita dapat menggunakan transisi bawaan D3 untuk memperbarui bagan dan menganimasikannya dengan mudah. Melanjutkan contoh di atas, menambahkan transisi pada width
semudah mengubah
bars.attr('width', function(d) { return d*widthScale});
ke
bars.transition().duration(1000).attr('width', function(d) { return d*widthScale});
Lebih baik lagi, jika kami mengizinkan pengguna untuk memasukkan kumpulan data baru, kami dapat menggunakan pilihan pembaruan D3 (masuk, perbarui, dan keluar) untuk juga menerapkan transisi ke data baru. Tapi bagaimana kita mengizinkan data baru? Jika Anda ingat, implementasi kami sebelumnya membuat bagan baru seperti ini:
d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);
Kami mengikat data ke pilihan D3.js, dan memanggil bagan kami yang dapat digunakan kembali. Setiap perubahan pada data harus dilakukan dengan mengikat data baru ke pilihan yang sama. Secara teoritis, kita bisa menggunakan pola lama dan menyelidiki seleksi untuk data yang ada, dan kemudian memperbarui temuan kita dengan data baru. Ini tidak hanya berantakan dan rumit untuk diterapkan, tetapi juga memerlukan asumsi bahwa bagan yang ada memiliki jenis dan bentuk yang sama.
Sebagai gantinya, dengan beberapa perubahan pada struktur fungsi generator JavaScript, kita dapat membuat bagan yang memungkinkan pemanggil dengan mudah meminta perubahan secara eksternal melalui metode chaining. Padahal sebelum konfigurasi dan data disetel dan kemudian dibiarkan tidak tersentuh, pemanggil sekarang dapat melakukan sesuatu seperti ini, bahkan setelah bagan diinisialisasi:
weatherChart.width(420);
Hasilnya adalah transisi yang mulus ke lebar baru dari bagan yang ada. Tanpa perhitungan yang tidak perlu dan dengan transisi yang ramping, hasilnya adalah klien yang bahagia.
Fungsionalitas ekstra ini hadir dengan sedikit peningkatan dalam upaya pengembang. Namun, upaya yang menurut saya sepadan dengan waktu secara historis. Berikut adalah kerangka bagan yang dapat diperbarui:
function barChart() { // All options that should be accessible to caller var data = []; var width = 800; //... the rest var updateData; var updateWidth; //... the rest function chart(selection){ selection.each(function () { // //draw the chart here using data, width // updateWidth = function() { // use width to make any changes }; updateData = function() { // use D3 update pattern with data } }); } chart.data = function(value) { if (!arguments.length) return data; data = value; if (typeof updateData === 'function') updateData(); return chart; }; chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; }; //... the rest return chart; }
Untuk melihat implementasi sepenuhnya, lihat di bl.ocks.org.
Mari kita tinjau struktur baru. Perubahan terbesar dari implementasi penutupan sebelumnya adalah penambahan fungsi update. Seperti yang telah dibahas sebelumnya, fungsi ini memanfaatkan transisi D3.js dan memperbarui pola untuk membuat perubahan yang diperlukan dengan lancar berdasarkan konfigurasi data atau bagan baru. Untuk membuat ini dapat diakses oleh pemanggil, fungsi ditambahkan sebagai properti ke bagan. Dan untuk membuatnya lebih mudah lagi, konfigurasi awal dan pembaruan ditangani melalui fungsi yang sama:
chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; };
Perhatikan bahwa updateWidth
tidak akan ditentukan hingga bagan diinisialisasi. Jika undefined
, maka variabel konfigurasi akan disetel secara global dan digunakan dalam penutupan grafik. Jika fungsi bagan telah dipanggil, maka semua transisi diserahkan ke fungsi updateWidth
, yang menggunakan variabel width
yang diubah untuk membuat perubahan yang diperlukan. Sesuatu seperti ini:
updateWidth = function() { widthScale = width / maxValue; bars.transition().duration(1000).attr('width', function(d) { return d*widthScale}); svg.transition().duration(1000).attr('width', width); };
Dengan struktur baru ini, data untuk bagan diteruskan melalui rantai metode seperti variabel konfigurasi lainnya, alih-alih mengikatnya ke pilihan D3.js. Perbedaan:
var weatherChart = barChart(); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);
yang menjadi:
var weatherChart = barChart().data(highTemperatures); d3.select('#weatherHistory') .call(weatherChart);
Jadi kami telah membuat beberapa perubahan dan menambahkan sedikit upaya pengembang, mari kita lihat manfaatnya.
Katakanlah Anda memiliki permintaan fitur baru: “Tambahkan dropdown sehingga pengguna dapat mengubah antara suhu tinggi dan suhu rendah. Dan buat warnanya juga berubah saat Anda melakukannya. ” Alih-alih menghapus grafik saat ini, mengikat data baru, dan menggambar ulang dari awal, sekarang Anda dapat membuat panggilan sederhana saat suhu rendah dipilih:
weatherChart.data(lowTemperatures).fillColor('blue');
dan nikmati keajaibannya. Kami tidak hanya menyimpan perhitungan, tetapi kami menambahkan tingkat pemahaman baru ke visualisasi saat diperbarui, yang sebelumnya tidak mungkin dilakukan.
Sebuah kata penting dari hati-hati tentang transisi diperlukan di sini. Berhati-hatilah saat menjadwalkan beberapa transisi pada elemen yang sama. Memulai transisi baru secara implisit akan membatalkan transisi yang berjalan sebelumnya. Tentu saja, beberapa atribut atau gaya dapat diubah pada elemen dalam satu transisi yang dimulai D3.js, tetapi saya telah menemukan beberapa contoh di mana beberapa transisi dipicu secara bersamaan. Dalam hal ini, pertimbangkan untuk menggunakan transisi serentak pada elemen induk dan anak saat membuat fungsi pembaruan Anda.
Perubahan Filsafat
Mike Bostock memperkenalkan penutupan sebagai cara untuk merangkum pembuatan grafik. Polanya dioptimalkan untuk membuat bagan yang sama dengan data berbeda di banyak tempat. Namun, dalam tahun-tahun saya bekerja dengan D3.js, saya menemukan sedikit perbedaan dalam prioritas. Alih-alih menggunakan satu contoh bagan untuk membuat visualisasi yang sama dengan data yang berbeda, pola baru yang saya perkenalkan memungkinkan pemanggil dengan mudah membuat beberapa contoh bagan, yang masing-masing dapat dimodifikasi sepenuhnya bahkan setelah inisialisasi. Selanjutnya, setiap pembaruan ini ditangani dengan akses penuh ke status bagan saat ini, memungkinkan pengembang untuk menghilangkan perhitungan yang tidak perlu dan memanfaatkan kekuatan D3.js untuk menciptakan pengalaman pengguna dan klien yang lebih mulus.