jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

SVG from Zero to Senior · Part 9 — Interactive & Data-Driven SVG with JavaScript

Make SVG think: generate shapes from a data array, the essential screen→user coordinate mapping with getScreenCTM().inverse() for correct hit-testing, pointer events, tooltips, and a small interactive chart from scratch.

Vì inline SVG chính là DOM, mỗi node là một element thật con có thể tạo, truy vấn, style, và gắn listener — y như một <div>. Đó là điều làm SVG thành xương sống của trực quan hoá dữ liệu: D3, Chart.js (renderer SVG), và đa số biểu đồ dashboard là SVG sinh từ dữ liệu. Hôm nay con tự dựng một cái nhỏ — và học cái mánh tách tương tác chạy đúng khỏi tương tác hỏng ngầm.

Hover các bar, click để đổi ngẫu nhiên một cái, shuffle dữ liệu, đổi bar/line — rồi mới đọc cách hoạt động.

Mở demo đầy đủ:

Tạo node SVG từ JS — cái bẫy namespace

Lỗi số một của người mới: dùng document.createElement('rect'). Element SVG sống trong một namespace XML khác, nên cần createElementNS:

const NS = 'http://www.w3.org/2000/svg';
const rect = document.createElementNS(NS, 'rect');   // ✅ correct
rect.setAttribute('x', 10);
rect.setAttribute('width', 40);
svg.appendChild(rect);

// document.createElement('rect') makes an unknown HTML element that never renders.

Để sinh cả biểu đồ, dựng chuỗi HTML rồi gán svg.innerHTML (như demo) đơn giản và đủ nhanh; dùng createElementNS khi con giữ tham chiếu lâu dài để cập nhật từng node.

Dữ liệu → hình: các hàm scale

Trái tim của mọi biểu đồ là hai hàm ánh xạ không gian dữ liệu sang không gian màn hình.

const data = [42, 78, 35, 90, 60, 25, 70];
const W = 320, H = 200, pad = 28;

// index → x position
const x = (i) => pad + (i + 0.5) * ((W - pad * 2) / data.length);
// value (0–100) → y position (remember: y grows DOWN, so invert)
const y = (v) => H - pad - (v / 100) * (H - pad * 2);

Rồi một .map() biến mỗi điểm dữ liệu thành một <rect> hoặc một điểm polyline. Đổi mảng, render lại — biểu đồ đi theo dữ liệu. Đó là toàn bộ ý tưởng đằng sau “data-driven.”

Cái mánh: toạ độ màn-hình → user

Đây là kiến thức senior ngăn lỗi quỷ quyệt nhất. Một pointer event cho con pixel màn hình (clientX/clientY). Nhưng hình của con sống trong user unit của viewBox, và CSS có thể đã scale SVG ra mọi kích thước. Nếu con so pixel với user unit trực tiếp, hit-test lệch ngay khi element không còn tỉ lệ 1:1.

Cách sửa là nghịch đảo ma trận biến đổi hiện tại của SVG:

function toUser(event) {
  const pt = svg.createSVGPoint();
  pt.x = event.clientX;
  pt.y = event.clientY;
  // getScreenCTM maps user → screen; invert it to go screen → user
  return pt.matrixTransform(svg.getScreenCTM().inverse());
}

Giờ toUser(e).x / .y là toạ độ viewBox thật, đúng ở mọi kích thước CSS hay zoom. Demo in chúng trực tiếp khi con di chuột. Nhớ kỹ — nó được hỏi trong phỏng vấn và làm hỏng dashboard thật.

Sự kiện: uỷ quyền hơn listener từng hình

Element SVG nổi bọt sự kiện như HTML, nên gắn một listener lên <svg> và dùng event.target.closest('.bar') thay vì một listener mỗi bar. Cách này sống sót khi re-render (node mới tự được phủ) và scale tới hàng nghìn hình.

svg.addEventListener('click', (e) => {
  const bar = e.target.closest('.bar');
  if (!bar) return;
  const i = +bar.dataset.i;     // stash the data index on the node
  data[i] = randomValue();
  render();                      // re-render from the new data
});

Cất chỉ số trong data-i là cây cầu giữa hình pixel được click và dòng dữ liệu của nó.

Tooltip

SVG không có widget tooltip sẵn, nên đặt một <div> HTML định vị tuyệt đối trên biểu đồ dùng toạ độ pixel của sự kiện (ở đây con không cần user coord — tooltip sống trong không gian màn hình). Cho tooltip thuần SVG, thêm một <g> với <rect> + <text> và di nó bằng transform.

Khi nào ngừng tự code

Cái này scale tới vài chục hay trăm hình rất đẹp. Quá vài nghìn node, DOM nặng — lúc đó chuyển sang <canvas> hoặc WebGL, hoặc dùng thư viện biết virtualize. Biết chỗ nào SVG thôi là công cụ đúng tự nó là phán đoán senior.

Lời cảnh báo của sư phụ

  • createElementNS, không bao giờ createElement cho node SVG.
  • Nghịch đảo CTM để hit-test. So clientX với user unit hỏng khi CSS scale.
  • Uỷ quyền sự kiện cho root. Listener từng node rò rỉ và chết khi re-render.

Luyện tập, không thì coi như chưa học

  1. Biểu đồ cột sửa được: render cột từ một mảng; click một cột để tăng giá trị rồi re-render.
  2. Đọc toạ độ chữ thập: dùng getScreenCTM().inverse() để hiện toạ độ user dưới con trỏ — rồi resize SVG bằng CSS và xác nhận vẫn đọc đúng.
  3. Sparkline từ fetch: fetch một mảng JSON nhỏ và vẽ thành <polyline>.

Phần tiếp theo

Giờ con sinh, tương tác, và trực quan hoá dữ liệu trong SVG được. Còn một thứ giữa con và senior: ship nó cho tốt. Ở phần kết, Phần 10, ta vào production: tối ưu bằng SVGO, khác biệt giữa cách giao inline / <img> / CSS background / sprite, hệ icon <symbol> + <use> với theme currentColor, khả năng truy cập (title/desc/role/aria), và ngân sách hiệu năng.