책에서 예제로 쓰고 있는 D3.js의 버전은 3.2.8이며 게시글에 작성된 예제들이 최신 버전의 API와 차이가 있을 수 있으니 참고 바랍니다.
데이터 시각화에서 반복적으로 수행하게 될 핵심 작업은 데이터 도메인에서 시각화 도메인으로 값을 매핑하는 작업이다. 따라서 효과적이고 정확하게 데이터 요소를 시각화 메타포(metaphore)로 매핑 하는 것이 데이터 시각화에서 가장 중요한 일이라고 할 수 있다.
짧게 말해서 스케일(scale)은 수학 함수로 생각할 수 있다. 이는 자바스크립트 함수처럼 명령형 프로그래밍 언어에서 정의된 함수와는 개념이 다르다. 수학에서 함수는 두 집합 사이의 매핑으로 정의된다.
위 정의로 미루어 보아 수학적 개념의 함수
는 우리가 해야할 일(시각화 도메인으로의 매핑)에 적합하다는 것을 알 수 있다. D3.js
에서는 이러한 스케일을 다양한 구조로 제공 하는데, 값의 매핑 뿐만 아니라 전환이나 축(axis)과 같은 다른 구성들로도 사용될 수 있다.
스케일을 사용함에 있어서 중요한 개념이 있는데, 바로 도메인(domain, 정의역)
과 범위(range, 치역)
이다. 우리는 D3.js
의 스케일을 통해 데이터 도메인 집합(domain)
에서 시각화 도메인 집합(range)
로 매핑할 수 있게 된다.
📝 아무래도 수학적인 개념이 들어간 내용이다 보니 글만 봐서는 이해가 부족할 수 있다. 좀 더 수학적인 개념의 접근을 보고 싶다면 책을 참고하길 바란다.
📝 책에서는 두 집합 사이의 관계에 대해서 설명하며 스케일(함수)은 두 집합 간의 한 요소에서 한 요소로의 매핑이라고 설명하고 있다.
D3.js
에서 가장 많이 사용하는 선형, 거듭제곱, 로그 스케일을 포함하는 양적 스케일에 대해 살펴보자.
우선 지난 포스트에서 다뤘던 입력-업데이트-종료
패턴으로 작성된 render
함수를 살펴보자.
// 1~10 까지의 요소를 포함하는 data 변수를 생성한다.
var max = 11, data = [];
for (var i = 1; i < max; ++i) data.push(i);
// 입력 인자로 데이터, 스케일 함수, DOM 셀렉터가 전달된다.
function render(data, scale, selector) {
// 입력
d3.select(selector).selectAll("div.cell")
.data(data)
.enter().append("div").classed("cell", true);
// 종료
d3.select(selector).selectAll("div.cell")
.data(data)
.exit().remove();
// 업데이트
d3.select(selector).selectAll("div.cell")
.data(data)
.style("display", "inline-block")
.text(function (d) {
// text 반환시 scale 함수가 적용된 것을 확인할 수 있다.
return d3.round(scale(d), 2);
});
}
위 로직에서 업데이트 패턴으로 작성된 chaining function
중 text
함수 내부에서 d3.round
함수와 함께 scale
함수가 사용된 것을 확인할 수 있다. d3.round
함수는 scale
함수의 결과를 반올림 해주는 함수이다.
위와 같은 형식으로 domain
을 range
로 변환하여 시각화 메타포로 매핑하는 것을 확인할 수 있다. render
함수를 먼저 확인했으니, 이제 scale 함수를 살펴보자.
d3.scale.linear
함수를 통해 기본 domain
집합 [0, 1]
과 기본 range
집합 [0, 1]
을 갖는 선형 양적 스케일을 생성할 수 있다. 기본 스케일은 본질적으로 숫자에 대한 항등 함수(identity function) 임을 보여준다.
📝 domain의 요소 값과 매핑된 range의 요소 값이 동일하기 때문에 기본 스케일은 항등 함수라고 한다.
기본 스케일로는 기대하는 결과를 만들어내지 못하므로 domain
함수와 range
함수로 추가적인 사용자 정의를 할 수 있다.
// 항등 스케일
var linear = d3.scale.linear()
.domain([1, 10])
.range([1, 10]);
domain 집합의 범위와 range 집합의 범위를 다르게 하여 스케일을 설정할 수도 있다.
// 선형 스케일
var linearCapped = d3.scale.linear()
.domain([1, 10])
.range([1, 20]);
d3.scale.pow
함수를 통해 지수가 1인 거듭제곱 스케일을 생성할 수 있다. exponent
함수를 통해 지수를 설정할 수 있다.
// 간단한 거듭제곱 스케일
var pow = d3.scale.pow().exponent(2);
다음 거듭제곱 스케일 함수는 roundRange
함수와 같이 작성되었다. roundRange
함수는 기본적으로 range
함수와 동일하지만 결과 값을 반올림 한다는 점이 다르다.
이는 데이터 시각화 요소로 매핑하는 작업을 할 때 px
단위로 변환 함에 있어서 유리하다. px
계산시 소수점 이하의 값을 피하는게 렌더링 과정에서의 anti-alias를 방지하는데 좋기 때문이다.
// 거듭제곱 스케일
var powCapped = d3.scale.pow()
.exponent(2)
.domain([1, 10])
.rangeRound([1, 10]);
d3.scale.log
함수를 통해 로그 스케일을 생성할 수 있다. 기본 로그 스케일은 밑을 10으로 가진다.
var log = d3.scale.log();
로그 스케일에서도 마찬가지로 domain
과 range
를 설정할 수 있다.
var logCapped = d3.scale.log()
.domain([1, 10])
.rangeRound([1, 10]);
D3.js
에서는 더 다양한 양적 스케일을 제공한다. 더 살펴 보고 싶다면 해당 링크를 참조 바란다.
가끔 시간과 날짜에 민감한 데이터 집합을 분석할 때가 있는데, D3.js
에서는 이런 유형의 매핑을 돕기 위해 내부적으로 시간 스케일을 제공한다.
var start = new Date(2013, 0, 1),
end = new Date(2013, 11, 31),
range = [0, 1200],
time = d3.time.scale()
.domain([start, end])
.rangeRound(range),
위 로직을 보면 d3.time.scale
함수를 통해 시간 스케일 함수를 생성하는 것을 확인 할 수 있다. 양적 스케일과 동일하게 시간 스케일도 마찬가지로 domain
, range
함수를 통해 설정할 수 있으며, 이 때 domain
집합의 범위는 특정 구간의 일자가 되는 것을 확인할 수 있다.
📝 시간과 관련된 데이터를 시각화 할 때, D3.js에서 제공해주는 시간 포매터(time formatter)를 사용할 수 있다. 특히 Date 객체를 사용할 때 굉장히 유용하다. 자세한 내용은 여기서 확인할 수 있다.
때때로 ["a", "b", "c"]
나 ["#1f77b4", "#ff7f0e", "#2ca02c"]
와 같이 데이터의 순서에 따른 매핑이 필요할 때가 있다. D3.js에서는 스케일을 사용해 이런 종류의 매핑을 할 수 있다.
var max = 10, data = [];
for (var i = 0; i < max; ++i) data.push(i);
var alphabet = d3.scale.ordinal()
.domain(data)
.range(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]);
d3.scale.ordinal
함수를 통해 순서 스케일이 생성됐다. 이 스케일의 domain
함수는 정수 배열을, range
함수는 a~j
까지의 문자열 배열을 전달받는다.
이런 식으로 손쉽게 스케일을 통해 매핑을 수행할 수 있다. 예를 들어 alphabet(0)
은 a
를 뜻하고 alphabet(4)
은 e
를 반환한다.
순서 스케일을 사용하면 파이 차트나 버블 차트에 각각의 다른 색상을 입히는 것들을 할 수 있다. 시각화에서는 다양한 요소에 다양한 색상을 설정하는 것이 매우 일반적인 작업이라 유용하게 사용할 수 있다.
📝 D3.js는 다양한 내장 순서 색상 스케일을 제공한다.