Skip to content

Instantly share code, notes, and snippets.

@ngminhtrung
Last active May 11, 2018 03:13
Show Gist options
  • Save ngminhtrung/62872e19e05c5f3120971caf748117db to your computer and use it in GitHub Desktop.
Save ngminhtrung/62872e19e05c5f3120971caf748117db to your computer and use it in GitHub Desktop.
Zoom and brush 01 - Vietnam CPI 2010 - 2016
height: 800
scrolling: yes
border: yes

Ghi chú:

  • Chart: Vietnam Consumer Price Index between year 2010 - 2016 (Chỉ số giá tiêu dùng Việt Nam 2010 - 2016)
  • Source of data: General Statistic Office of Vietnam (Nguồn dữ liệu: Tổng cục Thống kê Việt Nam)
  • Date of taken: 28th April 2018 (Ngày lấy dữ liệu: 2018.04.28)
  • Dựa theo Brush & Zoom của Mike Bostock.

Tính năng

Minh họa cách sử dụng

  • Area chart 01 (vùng màu xanh bên trên) để hiển thị giá trị của CPI trong từng năm (trục X ứng với thời gian, trục Y ứng với %).
  • Muốn xem chi tiết giá trị của từng tháng trong năm? Hãy di chuột vào vùng area 01, cuộn chuột giữa, hoặc vuốt touchpad đồ thị sẽ được phóng to/ thu nhỏ, chi tiết của từng tháng sẽ được hiện ra. Trục X và trục Y sẽ có thay đổi tương ứng.
  • Khi area 01 được co vào hoặc dãn ra, thì ô chữ nhật màu xám ở ngay bên dưới cũng co vào/ dãn ra theo một tỷ lệ nhất định. Việc thay đổi kích thước của vùng xám sẽ giúp điều chỉnh lại vùng area 01. Thay đổi như thế nào?
    • di chuột vào bên trong vùng xám, icon bàn tay hiện ra, giữ chuột trái kéo qua lại
    • di chuột đến 1 trong 2 đầu của vùng xám, icon mũi tên hai đầu hiện ra, kéo sang trái/ phải để thay đổi kích thước của vùng xám
    • di chuột ra bên ngoài vùng xám, nhưng vẫn bên trong area chart màu xanh nhỏ bên dưới (area chart 02), giữ chuột kéo rồi thả thành vùng xám mới.

Đánh số các vùng

Tại sao phải phức tạp như trên?

Nếu chỉ vẽ đồ thị dạng line/ area như bình thường, thì sẽ không tận dụng được sức mạnh tương tác trên nền web. Việc tạo ra vùng xám bên trong vùng xanh nhỏ bên dưới vô cùng hữu dụng, nó giúp người xem đánh giá mối tương quan giữa cây (dữ liệu chi tiết của một vài tháng trong năm) với rừng (chính là dữ liệu của cả 7 năm từ 2010 đến 2016).

Làm được như trên là nhờ cái gì?

Nhờ tính năng "zoom" và "brush" cung cấp bởi D3js.

Các bước vẽ chart

Lên layout và setup scale cho các trục x, y

Bước này giúp lên bố cục cho dữ liệu cần hiển thị.

layout

const svg = d3.select("svg"),
    margin = { top: 20, right: 20, bottom: 110, left: 40 },
    margin2 = { top: 430, right: 20, bottom: 30, left: 40 },
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    height2 = +svg.attr("height") - margin2.top - margin2.bottom;

const x = d3.scaleTime().range([0, width]),
    x2 = d3.scaleTime().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    y2 = d3.scaleLinear().range([height2, 0]);

Khởi tạo area để vẽ area-chart

  • Do ta dùng area chart để vẽ đồ thị nên cần khởi tạo area từ d3-shape thông qua d3.area().
  • Dựa vào các thiết lập .x, .y0, .y1, cũng như .curve(), d3.area() sẽ tính ra giá trị của path truyền vào cho attribute("d") ở phần bên dưới.
  • Do có 2 area chart (to và nhỏ, trên và dưới), nên cần tạo cả area và area2.
const area = d3.area()
            .curve(d3.curveMonotoneX)
            .x(function (d) { return x(d.date); })
            .y0(height)
            .y1(function (d) { return y(d.price); });

Khởi tạo vùng defs

  • Tất cả những gì được đặt trong <defs> sẽ chưa được hiển thị ngay trừ khi nó được gọi đến sau đó.
  • Bên trong <defs> là 1 hình chữ nhật <rect> nhưng gán với "clipPath". Một khi được kích hoạch, tất cả những gì đặt trong "clipPath" mới được hiển thị (trường hợp này là bên trong của hình chữ nhật), còn bên ngoài sẽ bị che đi.
  • Việc kích hoạt clipPath ở trường hợp này là thông qua phần external styll .area {clip-path: url(#clip);}
  • Tọa độ của clipPath không quan trọng. Chỉ cần kích thước của nó đúng bằng kích thước ta cần hiển thị, rồi kích hoạt nó bên trong vùng "area" nói trên, thì nó sẽ tự căn về tọa độ dựa theo "area".
  • Nếu không có clipPath? sẽ thấy phần area tràn ra bên ngoài vùng area (tất nhiên vẫn trong vùng svg). Từ đây hiểu là area là vùng bên trong hiển thị thông tin chính.
svg.append("defs").append("clipPath")
            .attr("id", "clip")
            .append("rect")
            .attr("width", width)
            .attr("height", height);
.area {
    fill: steelblue;
    clip-path: url(#clip);
}

Domain và scale

domainX:[
    "2009-12-31T17:00:00.000Z",
    "2016-11-30T17:00:00.000Z"]

domainX2:[
    "2009-12-31T17:00:00.000Z",
    "2016-11-30T17:00:00.000Z"]

Khởi tạo 2 behavior: zoom và brush

  • Brush giúp tạo 1 vùng tương tác thông qua thao tác của chuột hoặc vuốt chạm, ví dụ như click và kéo thả chuột. Gọi là vùng, nhưng nó có thể 1 chiều hoặc 2 chiều.
  • Việc click và kéo vùng brush có thể giúp dịch chuột 1 vùng khác của svg theo chủ đích của người lập trình.
const brush = d3.brushX()
    .extent([[0, 0], [width, height2]])
    .on("brush end", brushed);
  • Ở đây gọi d3.brushX(), nghĩa là chỉ tạo vùng 1 chiều dọc theo trục X.
  • brush.extent(): hạn chế vùng có thể brush tronng khoảng [[0, 0], [width, height2]].
  • Brush lắng nghe sự kiện thông qua .on("loại-sự-kiện", event-handler):
    • loại-sự-kiện ở đây là brushend:
      • brush: khi brush di chuyển (ví dụ khi mousemove), gọi event-handler;
      • end: khi brush dừng lại (ví dụ khi mouseup), gọi event-handler.
    • event handler ở đây là hàm brushed.
context.append("g")
    .attr("class", "brush")
    .call(brush)
    .call(brush.move, x.range());
  • Đoạn code trên có nghĩa là áp dụng brush vào context.g.brush, và nó tạo ra một vài <rect> mới nằm bên trong context.g.brush như sau. Lưu ý các attributes x, y, width, height cũng như thứ tự của từng <rect> để thấy chúng phục vụ cho các mục đích khác nhau.
    • <rect class="overlay">: nằm dưới cùng cùng, nếu di chuột vào vùng này cursor sẽ thành dạng crosshair. Kích thước của vùng này quy định bởi brush.extent.
    • <rect class="selection">: nằm giữa. Khi di chuột vào vùng này thì cursor sẽ thành bàn tay đang nắm. Vùng này có thể thay đổi thông qua brush.move(). Độ dài ban đầu của vùng này bằng x.range() nghĩa là bằng đúng độ dài ban đầu của x.
    • <rect class="handle handle--"> phụ thuộc vào brush ban đầu theo chiều nào. Trong bài này, do chỉ có brushX nên chỉ có 2 handle bên tây --w và đông --e. Khi di chuột vào 1 trong 2 vùng này (nằm ở đầu của rect.selection) thì sẽ hiện ra mũi tên 2 đầu ↔
<g class="brush" fill="none" pointer-events="all" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">
    <rect class="overlay" pointer-events="all" cursor="crosshair" x="0" y="0" width="900" height="40"></rect>
    <rect class="selection" cursor="move" fill="#777" fill-opacity="0.3" stroke="#fff" shape-rendering="crispEdges" x="294.7" y="0" width="172.1" height="40" style=""></rect>
    <rect class="handle handle--e" cursor="ew-resize" x="463.9" y="-3" width="6" height="46" style=""></rect>
    <rect class="handle handle--w" cursor="ew-resize" x="291.7" y="-3" width="6" height="46" style=""></rect>
</g>
  • Xem minh hoạ sau:

d3-brush-event

Brush chỉ liên quan đến vùng xám nhỏ hình chữ nhật ở bên dưới. Khi giữ chuột trái và kéo vùng xám này qua trái/ vùng phải, lúc này D3 nhận diện loại sự kiện này là brush. Chỉ cần vừa nhả chuột ra, D3 nhận diện loại sự kiện này là end.

Tại sao vùng xám này lại có thể thay đổi kích thước dựa khi ta phóng to thu nhỏ?. Chính là nhờ vào brush.move() như đã nói ở trên. Khi zoom, hàm zoomed() được gọi, group = context.select(".brush"), độ dài của phần rect.selection = x.range().map(t.invertX, t).

function zoomed() {
    // code ...
    const t = d3.event.transform;
    context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}

Tại sao khi thay đổi kích thước vùng xám lại làm thay đổi range của plot chính? Nhờ vào hàm brushed():

function brushed() {
    const s = d3.event.selection || x2.range();
    x.domain(s.map(x2.invert, x2));
    focus.select(".area").attr("d", area);
    focus.select(".axis--x").call(xAxis);
    svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
        .scale(width / (s[1] - s[0]))
        .translate(-s[0], 0));
}
  • Giúp người dùng phóng to, thu nhỏ một vùng nào đấy.
  • Tỷ lệ thu phóng được thiết lập từ 1 (nhỏ nhất) đến vô cùng thông qua zoom.scaleExtent([1, Infinity]).
  • Việc panning, dịch chuyển theo trục X được giới hạn bởi translateExtent(). Do tham số truyền vào là 1 mảng chứa 2 điểm góc [0, 0][width, height] cho nên việc kéo bản vẽ qua trái qua phải chỉ được thực hiện trong đúng vùng này. Thực tế là không thể kéo được. Nếu bỏ phần này đi thì sẽ kéo được sang trái/ sang phải, trục thời gian sẽ tự động được tính về trước 2011, hoặc sau 2016.
const zoom = d3.zoom()
    .scaleExtent([1, Infinity])
    .translateExtent([[0, 0], [width, height]])
    .extent([[0, 0], [width, height]])
    .on("zoom", zoomed);
date price
Jan 2010 101.36
Feb 2010 101.96
Mar 2010 100.75
Apr 2010 100.14
May 2010 100.27
Jun 2010 100.22
Jul 2010 100.06
Aug 2010 100.23
Sep 2010 101.31
Oct 2010 101.05
Nov 2010 101.86
Dec 2010 101.98
Jan 2011 101.74
Feb 2011 102.09
Mar 2011 102.17
Apr 2011 103.32
May 2011 102.21
Jun 2011 101.09
Jul 2011 101.17
Aug 2011 100.93
Sep 2011 100.82
Oct 2011 100.36
Nov 2011 100.39
Dec 2011 100.53
Jan 2012 101.00
Feb 2012 101.37
Mar 2012 100.16
Apr 2012 100.05
May 2012 100.18
Jun 2012 99.74
Jul 2012 99.71
Aug 2012 100.63
Sep 2012 102.20
Oct 2012 100.85
Nov 2012 100.47
Dec 2012 100.27
Jan 2013 101.25
Feb 2013 101.32
Mar 2013 99.81
Apr 2013 100.02
May 2013 99.94
Jun 2013 100.05
Jul 2013 100.27
Aug 2013 100.83
Sep 2013 101.06
Oct 2013 100.49
Nov 2013 100.34
Dec 2013 100.51
Jan 2014 100.69
Feb 2014 100.55
Mar 2014 99.56
Apr 2014 100.08
May 2014 100.20
Jun 2014 100.30
Jul 2014 100.23
Aug 2014 100.22
Sep 2014 100.40
Oct 2014 100.11
Nov 2014 99.73
Dec 2014 99.76
Jan 2015 99.80
Feb 2015 99.95
Mar 2015 100.15
Apr 2015 100.14
May 2015 100.16
Jun 2015 100.35
Jul 2015 100.13
Aug 2015 99.93
Sep 2015 99.79
Oct 2015 100.11
Nov 2015 100.07
Dec 2015 100.02
Jan 2016 100.00
Feb 2016 100.42
Mar 2016 100.57
Apr 2016 100.33
May 2016 100.54
Jun 2016 100.46
Jul 2016 100.13
Aug 2016 100.10
Sep 2016 100.54
Oct 2016 100.83
Nov 2016 100.48
Dec 2016 100.23
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment