Chart.js, Recharts, Highcharts — tiga nama itu biasanya muncul pertama kalau kamu googling cara bikin grafik di web. Semuanya bagus untuk keperluan standar. Masalahnya baru kelihatan ketika klien minta sesuatu yang nggak ada di dokumentasi: sparkline di tengah tabel, animasi urutan naik-turun, atau dua grafik yang saling meng-highlight saat di-hover. D3.js menangani kasus itu dengan pendekatan yang beda: dia nggak ngasih chart jadi, tapi alat untuk mengikat data langsung ke elemen SVG.

Apa Itu D3.js dan Kenapa Bukan Sekadar Chart Library

D3 singkatan dari Data-Driven Documents. Cara kerjanya sederhana: kamu kasih array data, D3 menghubungkannya ke elemen DOM yang sudah ada atau belum ada, lalu dia menambah, mengubah, atau menghapus elemen sesuai isi data. Karena kamu mengendalikan tiap atribut SVG, hampir tidak ada batasan bentuk chart yang bisa dibuat. Harganya: kode lebih panjang dibanding Chart.js. Untuk visualisasi yang benar-benar custom, itu harga yang sepadan.

Setup: Cukup Satu File HTML

Tidak ada build tool yang dibutuhkan. Buat folder baru berisi dua file — index.html dan app.js — lalu muat D3 versi 7 dari CDN.

html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>Belajar D3.js</title>
</head>
<body>
  <div id="chart"></div>
  <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
  <script src="app.js"></script>
</body>
</html>

Seleksi DOM ala D3

d3.select dan d3.selectAll bekerja mirip document.querySelector, tapi dengan API yang bisa dirantai. Kalau konsep DOM dan arrow function masih asing, sempatkan baca dasar JavaScript dulu. Contoh paling sederhana: tambah satu paragraf ke halaman, lalu warnai semua paragraf.

d3.select("body")
  .append("p")
  .text("Halo dari D3");

d3.selectAll("p")
  .style("color", "steelblue")
  .style("font-family", "sans-serif");

Data Join: Konsep yang Paling Penting

Tiga langkah klasik di D3: enter untuk data yang belum punya elemen, update untuk data yang sudah ada, dan exit untuk elemen yang datanya hilang. Metode .join() menyederhanakan ketiganya jadi satu baris. Perhatikan polanya: pilih elemen yang mungkin belum ada, ikat data dengan .data(), lalu .join() membuat atau menghapus elemen supaya jumlahnya pas dengan data.

javascript
const data = [4, 8, 15, 16, 23, 42];

d3.select("#chart")
  .selectAll("p")
  .data(data)
  .join("p")
  .text(d => `nilai: ${d}`);

Scale: Ubah Nilai Data Jadi Koordinat Piksel

Nilai 42 nggak bisa langsung jadi koordinat piksel — kamu butuh pemetaan. d3.scaleLinear() melakukannya: domain adalah rentang nilai data, range adalah rentang piksel. Setelah punya scale, tinggal panggil scale(nilai) untuk mendapat posisinya.

javascript
const scale = d3.scaleLinear()
  .domain([0, d3.max(data)])
  .range([0, 300]);

console.log(scale(0));   // 0
console.log(scale(42));  // 300
console.log(scale(21));  // 150

Bar Chart Lengkap dengan Sumbu

Sekarang gabungkan semuanya jadi bar chart. Kita pakai scaleBand untuk sumbu kategori (label hari) dan scaleLinear untuk nilai. Perhatikan cara menghitung tinggi batang: y(0) - y(value), bukan sekadar y(value).

javascript
const data = [
  { label: "Senin", value: 42 },
  { label: "Selasa", value: 65 },
  { label: "Rabu", value: 30 },
  { label: "Kamis", value: 80 },
  { label: "Jumat", value: 55 },
  { label: "Sabtu", value: 12 },
  { label: "Minggu", value: 47 },
];

const width = 480;
const height = 300;
const margin = { top: 20, right: 20, bottom: 40, left: 50 };

const svg = d3.select("#chart")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

const x = d3.scaleBand()
  .domain(data.map(d => d.label))
  .range([margin.left, width - margin.right])
  .padding(0.2);

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)])
  .range([height - margin.bottom, margin.top]);

svg.selectAll("rect")
  .data(data)
  .join("rect")
  .attr("x", d => x(d.label))
  .attr("y", d => y(d.value))
  .attr("width", x.bandwidth())
  .attr("height", d => y(0) - y(d.value))
  .attr("fill", "steelblue");

Batang sudah muncul, tapi belum jelas nilainya berapa. Tambahkan sumbu dengan d3.axisBottom dan d3.axisLeft, lalu tempel lewat .call().

javascript
svg.append("g")
  .attr("transform", `translate(0, ${height - margin.bottom})`)
  .call(d3.axisBottom(x));

svg.append("g")
  .attr("transform", `translate(${margin.left}, 0)`)
  .call(d3.axisLeft(y));

Satu jebakan yang bikin chart terbalik: di SVG, titik (0,0) ada di pojok kiri atas, jadi nilai y besar berarti posisi lebih ke bawah. Karena itu range sumbu y ditulis [height - margin.bottom, margin.top] — urutannya sengaja dibalik. Kalau kamu menulis [margin.top, height - margin.bottom], batangnya akan menggantung dari atas.

Line Chart dan Transisi

Untuk line chart, d3.line() menghasilkan string atribut d yang menyambung titik-titik data. Pasangkan dengan scaleLinear di sumbu x, tambah satu elemen path, dan kamu sudah punya grafik garis.

javascript
const points = [
  { x: 0, y: 12 }, { x: 1, y: 28 }, { x: 2, y: 20 },
  { x: 3, y: 45 }, { x: 4, y: 38 }, { x: 5, y: 60 },
];

const lineX = d3.scaleLinear()
  .domain([0, 5])
  .range([margin.left, width - margin.right]);

const lineY = d3.scaleLinear()
  .domain([0, d3.max(points, d => d.y)])
  .range([height - margin.bottom, margin.top]);

const line = d3.line()
  .x(d => lineX(d.x))
  .y(d => lineY(d.y));

svg.append("path")
  .datum(points)
  .attr("fill", "none")
  .attr("stroke", "crimson")
  .attr("stroke-width", 2)
  .attr("d", line);

Bagian serunya: D3 bisa menganimasikan perubahan data. Panggil .transition() sebelum mengubah atribut, dan D3 menginterpolasi dari nilai lama ke baru. Contoh ini memperbarui path saat data diganti, misalnya karena filter user.

javascript
const newPoints = [
  { x: 0, y: 8 }, { x: 1, y: 35 }, { x: 2, y: 15 },
  { x: 3, y: 50 }, { x: 4, y: 22 }, { x: 5, y: 55 },
];

svg.select("path")
  .datum(newPoints)
  .transition()
  .duration(750)
  .ease(d3.easeCubicInOut)
  .attr("d", line);

Ambil Data Langsung dari API

Data di dunia nyata jarang berupa array yang ditulis manual. D3 menyediakan d3.json() dan d3.csv() yang mengembalikan Promise, jadi kamu bisa pakai async/await seperti biasa. Kalau belum terbiasa dengan fetch dan Promise, ada panduan lengkapnya di artikel async/await JavaScript.

javascript
async function render() {
  const data = await d3.json("/api/penjualan.json");
  // gambar chart di sini dengan data
}

render();

Kalau proyekmu pakai React, jangan letakkan d3.select() di dalam return JSX — React dan D3 akan berebut kontrol atas DOM, dan itu sumber bug yang bikin pusing. Pola yang aman: simpan ref di container, gambar chart di dalam useEffect, lalu bersihkan lewat return function. Kalau useEffect masih asing, mulai dari dasar React sebelum menggabungkannya dengan D3.

Semua contoh di artikel ini jalan di satu file HTML tanpa dependency selain D3. Salin bar chart-nya, jalankan, lalu coba ini: ganti fill jadi gradasi berdasarkan nilai, tambah tooltip lewat event mousemove, atau pakai d3.scaleLog untuk data yang rentangnya jomplang. Rusak dulu, baru paham.