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.

Because inline SVG is just DOM, every node is a real element you can create, query, style, and attach listeners to — exactly like a <div>. {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>.} That is what makes SVG the backbone of data visualization: D3, Chart.js (SVG renderer), and most dashboard charts are SVG generated from data. {Đó 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.} Today you build a small one yourself — and learn the one trick that separates working interactivity from subtly broken interactivity. {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 the bars, click to randomize one, shuffle the data, toggle bar/line — then read how it works. {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.}

Open the full demo {Mở demo đầy đủ}: /tools/svg-interactive-demo/.

Creating SVG nodes from JS — the namespace gotcha {Tạo node SVG từ JS — cái bẫy namespace}

The number-one beginner mistake: using document.createElement('rect'). {Lỗi số một của người mới: dùng document.createElement('rect').} SVG elements live in a different XML namespace, so they need createElementNS: {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.

For generating a whole chart, building an HTML string and assigning svg.innerHTML (as the demo does) is simpler and fast enough; reach for createElementNS when you keep long-lived references to update individual nodes. {Để 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.}

Data → shapes: the scale functions {Dữ liệu → hình: các hàm scale}

The heart of any chart is two functions mapping data space to screen space. {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);

Then a .map() turns each datum into a <rect> or a polyline point. {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.} Change the array, re-render — the chart follows the data. {Đổi mảng, render lại — biểu đồ đi theo dữ liệu.} That’s the entire idea behind “data-driven.” {Đó là toàn bộ ý tưởng đằng sau “data-driven.”}

The trick: screen → user coordinates {Cái mánh: toạ độ màn-hình → user}

Here is the senior knowledge that prevents the most insidious bug. {Đây là kiến thức senior ngăn lỗi quỷ quyệt nhất.} A pointer event gives you screen pixels (clientX/clientY). But your shapes live in viewBox user units, and CSS may have scaled the SVG to any size. {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.} If you compare pixels to user units directly, hit-testing drifts the moment the element isn’t 1:1. {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.}

The fix is to invert the SVG’s current transformation matrix: {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());
}

Now toUser(e).x / .y are real viewBox coordinates, correct at any CSS size or zoom. {Giờ toUser(e).x / .y là toạ độ viewBox thật, đúng ở mọi kích thước CSS hay zoom.} The demo prints them live as you move the mouse. {Demo in chúng trực tiếp khi con di chuột.} Memorize this — it’s asked in interviews and breaks real dashboards. {Nhớ kỹ — nó được hỏi trong phỏng vấn và làm hỏng dashboard thật.}

Events: delegation beats per-shape listeners {Sự kiện: uỷ quyền hơn listener từng hình}

SVG elements bubble events like HTML, so attach one listener to the <svg> and use event.target.closest('.bar') instead of a listener per bar. {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.} This survives re-rendering (new nodes are automatically covered) and scales to thousands of shapes. {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
});

Stashing the index in data-i is the bridge between a clicked pixel shape and its data row. {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ó.}

Tooltips {Tooltip}

SVG has no native tooltip widget, so position an absolutely-placed HTML <div> over the chart using the event’s pixel coordinates (here we don’t need user coords — the tooltip lives in screen space). {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).} For a pure-SVG tooltip, append a <g> with a <rect> + <text> and move it with transform. {Cho tooltip thuần SVG, thêm một <g> với <rect> + <text> và di nó bằng transform.}

When to stop hand-rolling {Khi nào ngừng tự code}

This scales to dozens or low hundreds of shapes beautifully. {Cái này scale tới vài chục hay trăm hình rất đẹp.} Past a few thousand nodes, the DOM gets heavy — that’s when you switch to <canvas> or WebGL, or use a virtualization-aware library. {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.} Knowing where SVG stops being the right tool is itself senior judgment. {Biết chỗ nào SVG thôi là công cụ đúng tự nó là phán đoán senior.}

The master’s warnings {Lời cảnh báo của sư phụ}

  • createElementNS, never createElement for SVG nodes. {createElementNS, không bao giờ createElement cho node SVG.}
  • Invert the CTM for hit-testing. Comparing clientX to viewBox units breaks under CSS scaling. {Nghịch đảo CTM để hit-test. So clientX với user unit hỏng khi CSS scale.}
  • Delegate events to the root. Per-node listeners leak and die on re-render. {Uỷ quyền sự kiện cho root. Listener từng node rò rỉ và chết khi re-render.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. Editable bar chart {Biểu đồ cột sửa được}: render bars from an array; click a bar to increment its value and re-render. {render cột từ một mảng; click một cột để tăng giá trị rồi re-render.}
  2. Crosshair readout {Đọc toạ độ chữ thập}: use getScreenCTM().inverse() to show the user coordinate under the cursor — then resize the SVG with CSS and confirm it still reads correctly. {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 from fetch {Sparkline từ fetch}: fetch a small JSON array and draw it as a <polyline>. {fetch một mảng JSON nhỏ và vẽ thành <polyline>.}

What’s next {Phần tiếp theo}

You can now generate, interact with, and visualize data in SVG. {Giờ con sinh, tương tác, và trực quan hoá dữ liệu trong SVG được.} One thing remains between you and senior: shipping it well. {Còn một thứ giữa con và senior: ship nó cho tốt.} In the finale, Part 10, we go production: optimizing with SVGO, the difference between inline / <img> / CSS background / sprite delivery, <symbol> + <use> icon systems with currentColor theming, accessibility (title/desc/role/aria), and performance budgets. {Ở 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.}