G40N's blog

D3는 기본적으로 array형태로 묶인 데이터를 각각의 요소에 바인딩하고, 바인딩된 데이터를 통해 각 요소에 작업을 수행하는 라이브러리이다. 따라서, D3으로 차트를 만드는 모든 과정은 다음과 같은 과정을 거친다:
데이터를 바인딩할 element 선택 -> 데이터 바인딩 -> element가 대응되지 않는 데이터들에 진입 -> 데이터에 기반한 요소 추가 작업
여기서, 처음 차트를 생성할 때는 첫 단계에서 선택되는 데이터는 아무것도 없는 경우가 많다. 즉, 아무것도 없는(이제 element가 추가될) 빈 공간을 D3 selector를 통해 선택하고, 데이터를 바인딩한 후, 데이터 array 안에 존재하는 각각의 값을 이용하여 원하는 속성을 가지는 element를 하나하나 추가하는 방식으로 차트를 제작하는 방식이 보편적이라 할 수 있다.

예를 들어, 다음과 같은 표를 제작하려 한다고 하자.

name size(GB)
C: 128
D: 512
E: 16
F: 8
G: 8

먼저, 이 표를 구성하는 데이터를 입력받아야 한다. D3는 csv등의 다양한 포맷을 지원하지만, 결국 중요한 것은 최종적으로 Array 형태의 데이터를 만드는 것이다.
데이터가 csv파일이라면, 다음과 같이 구성되어 있을 것이다:

"name", "size(GB)";
"C:", 128;
"D:", 512;
"E:", 16;
"F:", 8;
"G:", 8;

이를 받아서 Array의 형태의 data를 생성하는 코드는 다음과 같다:

d3.csv("data.csv", function(error, data) {
  data.forEach(function(d, i) {
    d.name = d.name; // meaningless code
    d.size = +d.size;
  });

  // callback function
  draw(data);
});

d3.csv()함수는 첫 번째 argument의 string을 path로서 사용하여 해당 경로의 csv파일을 읽어들인 후 array로 변환하고, 이를 두 번재 argument로 설정된 함수에 data로서 넘긴다. 즉, 경로만 맞게 입력된다면 d3.csv()함수를 실행시키는 것만으로도 다음과 같은, Object들의 Array가 완성된다.

[
  {
    name: "C:",
    size: "128"
  },
  {
    name: "D:",
    size: "512"
  }
  /* ... */
];

이것만으로도 표를 구성하는 데에는 문제가 없지만, 만약 받아들인 데이터에 추가적인 작업을 하고 싶다면(예를 들어, size를 MB로 변환하고 싶다던가), forEach 함수를 활용하여 데이터를 수정할 수 있다. 위 코드에서는, d.size = +d.size라는 코드를 통해 문자열로 받아들여지는 size값을 숫자로 바꾸었다. (forEach에 argument로 반드시 d, i를 적을 필요는 없지만, D3의 Example 코드들을 보면, 많은 코드들이 암묵적으로 익명함수에 전달되는 argument를 d, i로 설정한다)

데이터를 로드하는 과정은 비동기적으로 이루어지기 때문에, d3.csv() 뒤에 있는 코드라인들은 데이터가 다 로드되지 않아도 계속해서 실행된다. 따라서, 데이터가 다 로드된 다음 테이블을 그릴 수 있도록 하려면, 함수의 인자로 전달된 익명함수의 끝에 콜백 함수를 지정해 주어야 한다. 여기서는 draw()함수가 바깥에 선언되어 있다고 가정하고, 그 함수를 호출하여 표를 그릴 수 있도록 하였다.

이제 표를 그리는 draw()함수를 살펴보면, 다음과 같은 구조를 띄고 있을 것이다.

columns = ["name", "size"];

function draw(data) {
  var table = d3
    .select("#div-for-table")
    .append("table")
    .attr("class", "table");

  var thead = table
    .append("thead")
    .attr("class", "thead")
    .append("tr")
    .selectAll("th")
    .data(columns)
    .enter()
    .append("th")
    .text(function(col) {
      return col;
    });

  var tbody = table
    .append("tbody")
    .selectAll("tr")
    .data(data)
    .enter()
    .append("tr");

  var cells = tbody
    .selectAll("td")
    .data(function(row, i) {
      return columns.map(function(col) {
        return row[col];
      });
    })
    .enter()
    .append("td")
    .text(function(d) {
      return d;
    });
}

처음의 table변수를 선언하면서 이루어지는 작업은 다음과 같다. 먼저, d3 selector로 table이 들어가게 될 공간을 선택한다. (d3 selector는 jquery와 기능이 거의 유사하다) 그 후, 그 자식 element로 <table>을 추가하고, 'table'이라는 class를 부여한다. 이 모든 과정은 코드에서 보이는 바와 같이 함수형 프로그래밍과 유사하게 짜여질 수 있다.

다음의 thead는 선언되면서 <table>th들을 추가한다. 먼저 위에서 선언된 table variable 안에 <thead> element를 추가하고 'thead' class를 부여한다. 거기에 다시 <tr> element를 추가하고, 그 안에서 selectAll()함수로 모든 <th> element를 선택한다. 이 때, <tr>안에는 아직 어떤 element도 들어가 있지 않으므로, 아무것도 선택되지 않게 되는데, 이 다음부터가 차트를 만드는 과저의 핵심이다. 이 선택된 element들에게 data()함수로 data array를 1:1로 바인딩한다. 그런데 selectAll()함수로는 아무것도 선택되지 않은 상태이기 때문에, 데이터의 전부가 매칭되지 않고 남게 된다. 바로 이 때, enter()함수가 사용된다.

이 함수는 매칭(바인딩)되지 않은 데이터에 하나하나씩 접근해, 그 데이터에 대해 동작을 수행할 수 있게 한다. 바인딩되지 않은 데이터들에 대한 forEach() 함수라고 보면 되겠다.

이제 enter()함수를 통해 각각의 데이터들에 진입한 상태인데, 뒤에 .append('th').text(...) 코드가 있으므로 각각의 데이터에 대해 <th> element가 추가된다. 즉, columns에 들어 있는 원소의 개수(2개)만큼의 <th>가 추가된다. enter함수는 뒤에 오는 함수들에게 인자로 지금의 데이터와 인덱스를 넘겨줄 수 있는데, 이 코드에서는 text() 함수 내부의 익명 함수가 col으로 데이터(문자열 'name'과 'size')를 받아 그 값을 반환한다. 최종적으로, 각각 'name''size'라는 문자열을 표시하는 <th> element가 <tr> 내부에 추가된다.

tbody도 위와 비슷한 역할을 하는데, 다른 점이 있다면 바인딩하는 데이터가 csv파일에서 직접 뽑아낸 데이터라는 것과, <thead><tr><th>를 추가하는 대신 <tbody> 안에 data의 길이와 같은 개수의 <tr>을 추가한다는 것이다.

cells의 경우, 위에서 선언된 tbody 안에 진입하여 <td>원소를 추가한다. 이때, 바인딩하는 데이터는 이미 각각의 row에 바인딩된 데이터를 기반으로 주어지는데, 만약 어떤 row에 바인딩된 데이터가 {'name': 'C:', 'size': 128}이라면, 익명 함수에 의해 반환되는 데이터는 ['C:', 128]로서, 이 데이터를 토대로 <td>, 즉 table 각각의 row 내부의 cell이 추가된다.

모든 작업이 끝나면 다음과 같은 html코드가 생성되며, 이는 우리가 만들려 한 표와 같은 구조를 이룰 것이다. 적당한 css파일을 갖고 있다면, 표의 각 원소에 적절한 class나 style을 줌으로써 표의 외양을 바꿀 수도 있다.

<table class="table">
  <thead class="thead">
    <tr>
      <th>name</th>
      <th>size(GB)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>C:</td>
      <td>128</td>
    </tr>
    <tr>
      <td>D:</td>
      <td>512</td>
    </tr>
    <tr>
      <td>E:</td>
      <td>16</td>
    </tr>
    <tr>
      <td>F:</td>
      <td>8</td>
    </tr>
    <tr>
      <td>G:</td>
      <td>8</td>
    </tr>
  </tbody>
</table>