Skala memetakan nilai data ke piksel, dan pembaruan menjaga pemetaan itu tetap benar ketika data berganti. Topik ini menjelaskan scaleLinear sebagai padanan skala gramatika, scaleBand untuk data kategori, pola pembaruan yang menghitung ulang domain lalu menjalankan join ulang, serta transisi beserta akibatnya terhadap persepsi perubahan. Setelah membacanya, pergantian tampilan pada grafik D3 dapat dijelaskan sebagai penghitungan ulang pemetaan.
Skala linear: domain dan range#
Fungsi d3.scaleLinear() membuat skala linear, yaitu fungsi yang memetakan domain data ke range tampilan. Pemanggilan domain([60, 95]) menyatakan rentang nilai ujian yang dipetakan, sedangkan range([0, 300]) menyatakan lebar tampilan yang tersedia. Rumus pemetaannya berupa garis lurus dari kedua ujung domain ke kedua ujung range.
Domain dan range dipisah karena keduanya berubah atas alasan yang berbeda. Domain mengikuti data, misalnya nilai terkecil dan terbesar ujian terbaru, sedangkan range mengikuti kanvas yang tersedia dan tidak berubah ketika datanya berganti. Diagram berikut memperlihatkan pemetaan dari domain 60 sampai 95 ke range 0 sampai 300 yang digunakan pada contoh sepanjang topik ini.
Skala diuji di Node#
Berkas berikut membuat skala linear dengan domain [60, 95] dan range [0, 300], lalu mencetak hasil pemetaan beberapa nilai ujian. Skala band untuk dua kelas turut dibuat dengan range [0, 80]. Jalankan berkas itu dari folder topik dengan perintah node skala.js, dan pustakanya dimuat dari folder tempat perintah dijalankan, yaitu folder yang memuat d3.min.js.
// skala.js: skala linear dan skala band dijalankan di Node, tanpa DOM.
// Jalankan dari folder topik: node skala.js
const d3 = require(process.cwd() + "/d3.min.js");
// nilai ujian (60 sampai 95) dipetakan ke lebar batang 0 sampai 300 piksel
const skalaNilai = d3.scaleLinear().domain([60, 95]).range([0, 300]);
const format = d3.format(".1f");
for (const nilai of [60, 77.5, 88, 95]) {
console.log(`nilai ${nilai} -> ${format(skalaNilai(nilai))} piksel`);
}
// dua kelas sebagai kategori: setiap kelas mendapat satu lajur selebar sama
const skalaKelas = d3.scaleBand().domain(["7A", "7B"]).range([0, 80]);
for (const kelas of ["7A", "7B"]) {
console.log(`kelas ${kelas}: mulai ${skalaKelas(kelas)}, lebar ${skalaKelas.bandwidth()}`);
}nilai 60 -> 0.0 piksel
nilai 77.5 -> 150.0 piksel
nilai 88 -> 240.0 piksel
nilai 95 -> 300.0 piksel
kelas 7A: mulai 0, lebar 40
kelas 7B: mulai 40, lebar 40Keluarannya mengonfirmasi rumus di atas. Nilai 77.5 berada tepat di tengah domain 60 sampai 95, sehingga hasilnya tepat 150, sedangkan nilai 88 yang berjarak 80 persen dari batas bawah menghasilkan 240. Skala band membagi 80 piksel untuk dua lajur, sehingga kelas 7A mulai di 0 dan kelas 7B mulai di 40, keduanya selebar 40 piksel. Satu perilaku perlu dicatat: nilai di luar domain tetap dipetakan mengikuti rumus yang sama, sehingga nilai ujian 100 akan menghasilkan lebar lebih dari 300 piksel dan domain tetap harus ditetapkan secara sengaja.
Skala band untuk kategori#
Nilai ujian berjalan pada garis angka, sedangkan kelas seperti 7A dan 7B tidak. Data kategori dipetakan dengan scaleBand, yaitu skala yang membagi range menjadi lajur sebanyak kategori pada domainnya. Setiap kategori mendapat satu lajur dengan posisi awal dan lebar yang dihitung dari jumlah kategori, sehingga batang kategori dapat diletakkan berdampingan tanpa penghitungan jarak manual.
Skala ini menjadi dasar hampir semua grafik batang berkategori: domainnya berisi nama kategori, dan batangnya diletakkan pada lajur masing-masing dengan lebar yang diambil dari bandwidth(). Metode paddingInner menyisakan jeda antarlajur dengan mengorbankan sebagian lebarnya. Apabila jumlah kategori berganti, seluruh lajur menyesuaikan karena lebarnya dihitung ulang dari panjang domain, dan kebiasaan gramatika yang sama tetap berlaku, yaitu posisi untuk nilai dan lajur untuk kategori. Dalam praktiknya, skala band sering dipasangkan dengan skala linear: band menyediakan lajur sebagai wadah batang, sedangkan skala nilai menghitung tinggi batang di dalam lajur tersebut.
Pola pembaruan: hitung ulang, lalu join ulang#
Pembaruan grafik interaktif mengikuti dua langkah yang selalu sama: hitung ulang skala apabila data berganti, lalu jalankan join yang sama. Demo berikut menerapkannya pada tiga keadaan data, yaitu dua belas nilai kelas 7A, dua belas nilai kelas 7B, dan lima nilai pertama kelas 7B. Setiap render menghitung domain baru dari nilai terbesar data melalui d3.max, kemudian menjalankan join yang sama dengan transisi 500 milidetik. Domain yang terhitung ditampilkan pada teks status.
Dua perilaku patut diamati. Pertama, peralihan kelas 7A ke kelas 7B mengubah domain dari [0, 95] menjadi [0, 91], sehingga batang untuk nilai yang sama berubah tingginya karena pemetaannya berganti, dan perubahan tampilan dengan demikian tidak selalu berarti perubahan nilai. Kedua, penyaringan lima siswa mengubah domain menjadi [0, 87] dan menghapus tujuh batang melalui grup exit, sedangkan batang yang tersisa diregangkan untuk mengisi pemetaan yang baru. Pola hitung ulang lalu join ulang inilah yang membuat grafik yang sama dapat merespons data baru maupun perubahan ukuran layar, karena keduanya hanya mengubah argumen skala sebelum join dijalankan.
Panduan sumbu dari skala#
// tidak-dijalankan
const sumbuX = d3.axisBottom(skalaX).ticks(6).tickFormat(d3.format("d"));
svg.append("g")
.attr("transform", "translate(0, 220)")
.call(sumbuX);Skala hanya menghitung posisi, sedangkan panduan menerjemahkannya kembali untuk pembaca. Demo pada bagian sebelumnya sengaja tidak menampilkan sumbu agar perhatiannya tertuju pada perubahan batang, sedangkan grafik untuk pembaca sungguhan hampir selalu menyertakannya. Modul d3-axis menyediakan pembuat sumbu yang menerima skala yang sudah jadi: axisBottom menggambar garis horizontal, penanda tick pada nilai yang dipilih skala, dan label dalam satuan asli data.
Argumen ticks(6) berupa permintaan jumlah penanda, bukan perintah mutlak, karena skala memilih nilai yang bermakna di sekitar angka itu. Elemen g yang dibuat sebelum call() menjadi wadah gambar sumbu, dan transformasinya menempatkan garis tersebut di bawah area gambar. Karena sumbu menunjuk ke skala, pemanggilan ulang sumbu setelah domain dihitung ulang membuat penanda dan labelnya ikut menyesuaikan, sehingga panduan dan batang tidak pernah berpisah pemetaannya. Padanan gramatika untuk lapisan panduan dengan demikian bukan komponen jadi, melainkan pembantu yang dirangkai ke skala miliknya sendiri.
Transisi dan persepsi perubahan#
Metode transition() menganimasikan perubahan atribut selama durasi yang diberikan, dan demo di atas menggunakan 500 milidetik. Selama transisi berjalan, atribut awal dan atribut akhir disambung dengan nilai antara yang dihitung, sehingga pembaca melihat batang mana yang naik dan turun. Kemudahan mengikuti perubahan ini menjadi salah satu alasan pola hitung ulang lalu join ulang digunakan secara luas.
Durasi dan pelunakan transisi adalah pilihan yang menyangkut kejujuran. Perubahan besar yang dianimasikan terlalu lambat dapat terasa seperti penyesuaian kecil, sedangkan perubahan kecil yang dianimasikan terlalu cepat dapat luput sama sekali. Nilai antaranya juga bukan data: pembaca melihat batang bergerak melewati posisi-posisi tengah, tetapi tidak ada jaminan data tersebut pernah ada, misalnya ketika rata-rata ujian satu semester dianimasikan menuju semester berikutnya. Karena itu durasi dipilih secukupnya, perubahan yang ditunjukkan harus berasal dari data yang sungguh berganti, dan angka akhir tetap tersedia melalui label atau tabel di dekat grafik.
Kesalahan umum#
- Domain ditulis tetap padahal data berganti. Batang melebihi kanvas ketika nilai terbesar yang baru melampaui domain, atau malah mengecil ketika datanya sempit. Hitung domain dari data melalui
d3.maxdand3.minpada setiap render. - Memanggil d3 sebelum pustakanya dimuat. Skrip yang berada di atas
<script src="d3.min.js">dijalankan lebih dulu, dan pemanggilan d3 pada saat itu menghasilkanReferenceError. Urutan skrip demo pada topik ini selalu memuat pustaka lebih dahulu. - Menganimasikan sebelum skala dihitung ulang. Transisi berjalan mulus menuju posisi yang keliru, dan kehalusan animasinya justru menyembunyikan perhitungan yang salah. Hitung ulang domain terlebih dahulu, karena transisi tidak memperbaiki pemetaan.
- Membaca nilai antara transisi sebagai data. Angka di tengah animasi merupakan hasil interpolasi, bukan pengukuran, sehingga keadaan di tengah jalan tidak boleh dibaca sebagai riwayat nilai. Pembacaan nilai hanya dilakukan pada keadaan awal dan akhir, dengan angka akhirnya diberi label.
Ringkasan#
scaleLinearmemetakan domain data ke range tampilan dengan rumus linear, dan pemisahan keduanya menjaga data dan kanvas berubah atas alasan masing-masing.scaleBandmembagi range menjadi lajur per kategori, dengan posisi dan lebar yang dihitung dari jumlah kategori.- Pola pembaruan berupa hitung ulang skala lalu join ulang, dan pola yang sama menjadikan grafik responsif terhadap data baru maupun kanvas baru.
- Transisi menganimasikan perubahan, tetapi durasinya memengaruhi persepsi dan nilai antaranya bukan data, sehingga durasi dipilih secukupnya dengan angka akhir tetap tersedia.
Referensi#
- d3-scale — D3
— Dokumentasi resmi
scaleLinear,scaleBand, dan panduan memilih jenis skala. - d3-array — D3
— Dokumentasi
d3.max,d3.min, dan fungsi ringkasan untuk penghitungan domain.