D3 (Data-Driven Documents)는 데이터 시각화 라이브러리로

다양한 차트를 쉽게 제작할 수 있다.

D3로 만들 수 있는 차트는 githubbl.ocks.org에서 확인해 볼 수 있다.

인터넷에서 찾을 수 있는 대부분의 예제는 v3 버전으로 제작되었고

최신 버전인 v4는 관련된 예제가 많지 않다.

v3와 v4는 척도(scale) 사용에 변화가 있고

대부분 유사하게 사용할 수 있다.

자세한 차이는 여기를 읽어보길 바란다.


실제 개발에서는 D3(v3)를 이용하여

무료로 제공하는 JUI, C3 등의 차트 라이브러리가 있으니

이 라이브러리를 사용하는 것이 좋을 것이다.


여기서는 개발 능력을 높이고 자신에게 맞는 차트를 제작하기 위하여

컬럼 차트(Column Chart) 예제를

v4를 이용하여 단계별로 제작하며 정리할 예정이다.


예제를 익히기 위해서는

select, selectAll, enter, data, append, domain, range 등의

기본적인 함수는 알고 있다는 전제로 시작하기 때문에

모른다면 찾아본 후에 진행하는 것이 좋다.

이 함수들에 대한 자세한 설명은 정리하지 않지만

사용법을 익히는 예제로 구성되어서 보는데는 문제가 없을 것이다.


첫번째 예제로

주어진 데이터(dataset)의 값을 화면에 출력하는 예제이다.

각 데이터는 차트로 보이기 위해 사각형(rect)을 생성(append)하였다 [라인 12].


컬럼 차트이기 때문에

값(dataset[i])에 따라 도형(rect)의 크기(height)가 바뀌고 [라인 13]

데이터 순서에 따라 생성한 도형의 위치(x)가 지정된다 [라인 15].

값의 크기는 그대로 도형의 크기로 지정했지만

순서를 그대로 위치(x)로 사용하면 도형이 모두 붙어버리기 때문에

도형의 너비(40)보다 조금 크게(50 * i) 지정하였다 .


마지막으로 도형을 주어진 데이터 개수 만큼(dataset.length)

반복해서 생성하기 위해 for문을 사용하였다 [라인 11].

예제를 실행하면 생성된 바(rect)가

위에서 아래로 향하고 있는 문제를 확인할 수 있다.

웹에서 좌표는 좌측 상단을

기준 (0, 0)으로 좌표를 지정하여 구현하기 때문이다.

(상세한 설명은 아날로그 시계 참조)

즉, y좌표를 지정하지 않았기 때문이다.

    svg.append("rect")
        .attr("height", dataset[i])
        .attr("width", 40)
        .attr("x", 50 * i)
        .attr("y", 100 - dataset[i]);

전체코드보기

차트의 바닥을 y좌표가 100 인 지점으로 임의 지정 하고,

(데이터의 최대값이 39 이므로 39 보다 크면 된다.)

각 도형의 y좌표 값을 100 에서 자신의 크기를 뺀 값으로

지정하면 다음 그림과 같이 실행된다.

dataset이 [9, 19, 29, 39, 29, 19, 9]로 되어 있으니

100에서 각 값으 빼면

[91, 81, 71, 61, 71, 81, 91]이 된다.

즉, 91px에서 9px 크기(height)의 도형을 생성한다.


다음으로 데이터의 개수 만큼 도형을 생성할 때

for문을 이용하였다.

이건 일반적인 자바스크립트를 이용한 코딩 방법이고

D3 (Data-Driven Documents)의 특징을 반영한 것이 아니다.

D3는 이름 그대로 데이터 기반,

즉 데이터 처리를 쉽게 하는 것이 목적이기 때문에 다음과 같이 작성해야 한다.


D3에서는 사용할 데이터를 지정하면 [라인 11]

각 데이터별(enter)로 실행해서 도형을 생성할 수 있다 [라인 12].

다만, 데이터 값을 가지고 오는 방식이

배열(dataset[i])로 지정할 수도 있고

함수의 파라미터로 넘어오는 값(d)을 이용할 수도 있다.

(여기서 사용된 함수를 callback함수라고 한다.)

도형의 크기(height)는 파라미터 값을 이용했고 [라인 13]

도형의 위치는 (y)는 배열을 이용했다 [라인 16].

이해를 위해서 두가지 방법을 사용했지만

일반적으로 파라미터 값을 이용한다.


이번에는 다음 그림과 같이 차트를 보다 크게 구현해 본다.

이것은 도형의 크기를 5배 크게(d*5) 하면 간단하게 구현된다.

다만, 도형이 커졌으니 차트의 바닥을 100에서 250으로 바꾸어 주었다.

그렇게 않으면 도형의 윗 부분이 잘려서 출력된다.

svg.selectAll("bar")
    .data(dataset)
    .enter().append("rect")
        .attr("height", function(d, i) {return (d*5)})
        .attr("width", 40)
        .attr("x", function(d, i) {return (50 * i)})
        .attr("y", function(d, i) {return (250-d*5)});

전체코드보기


이번에는 그림과 같이

바 차트에 원하는 색을 지정하고

바에 마우스를 올리면 다른 색으로 바뀌게 작성해 본다.

이것은 프로그램으로 처리하는 것 보다

CSS로 간단하게 구현할 수 있다.

일반적인 HTML 태그(예 div)는

배경색을 background-color에 지정하는데

SVG (D3)에서는 fill로 지정한다.

따라서, 생성한 바(rect)에 클래스를 추가하고 .attr("class", "bar")

스타일에서 bar 클래스에 fill 속성 값을 지정해 주면 된다.

마우스가 올라간 바(rect)의 색을 바꾸는 것은

hover선택자를 이용한다.

<style>
.bar {
    fill: skyblue;
}
.bar:hover {
    fill: blue;
}
</style>

<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var dataset = [9, 19, 29, 39, 29, 19, 9];
var svg = d3.select("svg");
svg.selectAll("rect")
    .data(dataset)
    .enter().append("rect")
        .attr("class", "bar")
        .attr("height", function(d, i) {return (d*5)})
        .attr("width", 40)
        .attr("x", function(d, i) {return (50 * i)})
        .attr("y", function(d, i) {return (250-d*5)});
         
</script>

전체코드보기

이외에도 바의 선 색(stroke) / 굵기(stroke-width), 투명도(fill-opacity) 등

차트를 보기 좋게 처리하는 것을 CSS로 구현할 수 있다.

.bar {
    fill: skyblue;
    fill-opacity: 0.3;
    stroke: skyblue;
}
.bar:hover {
    fill-opacity: 1;
}

전체코드보기

이번에는 그림과 같이 도형에 맞는 값을 출력한다.

제작하는 방법은

rect를 생성하는 것과 동일한데

rect대신에 text를 생성하면 된다.

svg.selectAll("rect")
    .data(dataset)
    .enter().append("rect")
        .attr("class", "bar")
        .attr("height", function(d, i) {return (d*5)})
        .attr("width", 40)
        .attr("x", function(d, i) {return (50 * i)})
        .attr("y", function(d, i) {return (250-d*5)});

svg.selectAll("text")
    .data(dataset)
    .enter().append("text")
    .text(function(d) {return d})
        .attr("x", function(d, i) {return 50 * i})
        .attr("y", function(d, i) {return 250-d*5});

전체코드보기

기존의 rect를 생성하는 코드 다음에

위 코드에서 작성된 것처럼 text 생성 코드만 추가하면 된다.

rect와 text 생성이 동일하게 작성되었다.

text는 높이와 너비가 필요 없고,

별도의 CSS 클래스(class)를 지정하지 않았다.

그리고, text 함수를 호출하여 데이터 값을 넣어준다.


위 그림에서 보는 것처럼

출력된 값이 왼쪽에 치우쳐 있어 보기에 좋지 않다.

그리고, 다음 그림처럼 값을 도형 안에 출력해 본다.

<style>
~~생략 ~~
.text {
    fill: white;
    font-weight:bold;
}

</style>
~~생략 ~~
svg.selectAll("text")
    .data(dataset)
    .enter().append("text")
    .text(function(d) {return d})
        .attr("class", "text")
        .attr("x", function(d, i) {return 50 * i + 10})
        .attr("y", function(d, i) {return 250-d*5 + 15});

전체코드보기

도형안에 출력하기 위해 Y좌표에 글자의 높이(15) 만큼 더해주고,

도형의 중앙에 출력하기 위해 X좌표에 글자의 너비(10) 만큼 더해서 지정했다.

그리고, 글자를 조금 더 보기 좋게 하기 위해

CSS로 text를 생성하여 지정하였다.

일반적인 HTML에서는 글자색이 font-color이지만

D3 (실제론 SVG)에서는 fill로 지정한다.




'JavaScript > Chart' 카테고리의 다른 글

2. D3 (SVG) 차트 만들기 - Bar II  (0) 2017.04.15
3. D3 (SVG) 차트 만들기 - Line  (6) 2017.04.15

앞서의 예제는 기본적인 D3의 개념과 사용법을 익히기 위한 것이었다.

여기에서는 일반적인 바(column) 차트에서

흔히 보는 주요 기능 몇 가지(척도 중심)를 구현하면서

D3에 대한 보다 깊은 개념과 사용법을 정리한다.

1. X / Y 축 (척도) 만들기

2. 그리드 만들기

3. 도움말 만들기

이러한 기능을 구현하기 위해서

앞서의 예제 중에서 마지막 예제를 다음과 같이 수정해 준다.


차트의 가로(X)와 세로 (Y) 축에

주어진 데이터의 값이 출력되는 것이

보기에도 좋고 차트에서 일반적으로 제공되는 기능이기도 하다.

따라서, 이전에는 데이터를 세로 축(Y)의 값만 배열로 작성했지만

가로축(X)에도 값을 출력하기 위해 Json을 배열로 작성했다 [라인 24].

예로, X축의 값 A, Y축의 값 9와 같이 ({x:'A', y:9})

X, Y의 값을 하나의 세트로 구성하였다.


이러한 데이터 구조의 변화에 따라,

33, 36, 44 라인에 작성된 것 같이 사용법에 변화가 있다.

        .attr("height", function(d, i) {return (d.y*5)})
        .attr("y", function(d, i) {return (250-d.y*5)});
        .attr("y", function(d, i) {return 250-d.y*5 + 15});

바의 높이와 위치를 계산할 때

변수 d로 받아서 사용하던 데이터 사용법을 바꾸어 주어야 한다.

D3에서 data 지정을 배열로 하면

각각의 속성 지정에 넘어오는 값은 각 배열의 원소(d)가 넘어오고,

이 배열 원소는 그냥 값이니 그대로 사용하면 된다.

즉, d * 5와 같이 바로 사용했다.

수정한 예제는 Json 배열이니 각 원소는 Json이 되고

Json은 x, y로 지정되어 있기 때문에

d.y * 5와 같은 방식으로 사용되게 된다.


이상의 코드는 앞서의 예제를 배열에서 Json으로 바꾼 것 외에

다음 실행 결과와 같이 CSS를 이용하여 SVG 테그에 외곽선을 그렸다 [라인 4].

외곽선이 있는 실행 결과를 보면

차트가 왼쪽으로 치우친 것을 알 수 있다.

즉, 주어진 SVG의 크기에 맞게 제작되지 않았다.

앞서의 예제에서 크기에 맞춰서 바(rect)를 생성하는 수식을 사용하지 않았다.

즉, 각 개별 바의 크기는

바의 간격과 바의 수 (데이터의 수)를 고려하여

SVG의 크기(width)에 맞게 계산해야 한다.

이전 예제에서는 바의 크기(width)는 40, 간격은 10으로 고정해서 구현했다.

바의 높이도 주어진 값에 5를 곱했을 뿐이다.

바의 높이는 주어진 값 중 최고 값(Max)과

SVG의 높이(height)를 비율로 계산해서 처리해야 한다.


서술이 길었는데

핵심은 제법 복잡할 수 있는 이러한 처리를

D3에서는 간단한 코드 몇 줄로 구현할 수 있다는 것이다.

먼저 다음 그림과 같이 

SVG 크기에 맞추어 X축부터 출력해 본다.


기능을 구현하기 전에 D3에서 제공하는 scale에 대해서 알아야 하는데

v3에서 v4로 바뀌면서 변화된 것이 많고

종류도 많은데 정리된 자료가 별로 없다.

D3의 주요 기능 중 하나이니

영문 자료라도 확인해 두면 도움이 될 것이다.

v4에 대한 자료는 별로 없지만

v3는 검색해 보거나 다음 자료들을 참고하면 도움이 될 것이다.

여기서는 필요한 기능만 정리하고 넘어간다.

두 버전의 차이를 간단하게 정리하면

v3에서는 scale(척도)과 axix(축)을 나누어서 구현했다.

v4에서는 하나(scale)로 처리하는 차이가 있다.


v4에 대한 영문 자료를 대충만 봐도 알 수 있지만

아주 많은 척도가 정의되어 있다.

이 중에서 scaleBand라는 척도가 사용된다는 것만 기억하고 넘어간다.


척도에서 중요한 개념(함수)이 domain과 range로

v3와 v4에서 같은 개념과 방식으로 사용된다.

domain은 데이터 값들의 정보(범위, 값)를 지정하고,

range는 출력되는 화면의 정보(범위, 픽셀)을 의미한다.

즉, 척도는 데이터 정보과 출력 정보를 결합하여 계산하는 역활을 하는 것이다.

예로, 1부터 5까지의 값을 가지는 데이터가 있고

출력하고자 하는 차트(SVG)의 너비가 100픽셀(px)이라고 하면,

domain([1,5])가 되고 range([1,100])로 작성한다.

그리고, 척도(scaleBand)에서

데이터 값이 1일때는 20픽셀, 2일때는 40픽셀과 같은

적당한 위치값을 계산해서 반환하게 된다.

즉, 척도(scaleBand)라는 클래스에

데이터 값(domain)과 출력 범위(range)를 넣어주고

필요한 값을 꺼내어 사용하면 된다.

필요한 값은 한 데이터(차트 도형, Rect)의 크기와 위치이다.

크기는 x축이니 width로 bandwidth란 메소드를 이용하고

위치는 척도의 인스턴스(xScale) 함수를 호출하면 된다.


이러한 기초 지식을 가지고 다음 코드를 이해해 본다.

앞서 정리한 내용을 코드로 작성하고 빨강색으로 표시하였다.

var dataset = [{x:'A', y:9 }, {x:'B', y:19}, {x:'C', y:29}, {x:'D', y:39},
                {x:'E', y:29}, {x:'F', y:19}, {x:'G', y:9 }];
var svg = d3.select("svg");
var width  = parseInt(svg.style("width"), 10);
var height = parseInt(svg.style("height"), 10)-20;
var xScale = d3.scaleBand()                                        
    .domain(dataset.map(function(d) { return d.x;} ))
    .range([0, width]).padding(0.2);
   
svg.selectAll("rect")
    .data(dataset)
    .enter().append("rect")
        .attr("class", "bar")
        .attr("height", function(d, i) {return (d.y*5)})
        .attr("width", xScale.bandwidth())                            
        .attr("x", function(d, i) {return xScale(d.x)})
        .attr("y", function(d, i) {return (height-d.y*5)});
   
svg.selectAll("text")
    .data(dataset)
    .enter().append("text")
    .text(function(d) {return d.y})
        .attr("class", "text")
        .attr("x", function(d, i) {return xScale(d.x)+xScale.bandwidth()/2})
        .style("text-anchor", "middle")
        .attr("y", function(d, i) {return height-d.y*5 + 15});
   
svg.append("g")                                                      

    .attr("transform", "translate(0," + (height) + ")")
    .call(d3.axisBottom(xScale));

전체코드

① domain에 지정하는 데이터 값이

연속된 값이면 시작값과 종료값(최대값)을 지정하면 된다.

여기서는 A, B, C 등의 문자열이기 때문에

배열(dataset)에 사용된 값 모두를 나열하여(map) 지정하였다.

range의 화면 범위를 지정하기 위해

SVG의 CSS width값을 구해서 지정하였다 (svg.style("width")).


② 바를 생성하면서(append("rect"))

바의 크기(attr-width)와 위치(attr-x)를 척도를 이용해서 지정한다.

이렇게 척도를 이용하여 차트의 크기(SVG)나

입력되는 데이터의 개수에 따라 적절한 도형(rect)이 생성되게 된다.

배열 개수를 바꿔서 확인 해보길 바란다.


③ 마지막으로 척도를 이용하여 축(axis)을 작성한다.

X축이니 화면(SVG) 바닥(bottom)에 놓고

위치는 화면의 높이(height = Height-20)가 된다.

즉, translate를 이용하여 X 좌표는 0, Y 좌표는 height인 지점에

라인을 그리고 축의 값(xScale)을 출력한다.

height에 20px을 뺀 것은 X 축의 값을 출력하는 공간을 계산한 것이다.


지금까지 사용하지 않은 그룹 개념이 사용되었다 (svgG.append("g")).

눈금(tick)은 여러 개의 선으로 구성되기 때문이다.

즉, 눈금 개수(tick count) 만큼 SVG 선(Line)이 생성된다.

이것을 간편하게 관리하기 위해 그룹으로 관리한다.

웹 브라우저의 개발자 도구(F12)로 확인하면

SVG group (X축)에 여러 개의 눈금(Line)이 있는 것을 확인할 수 있다.

그림을 보면 몇개의 그룹이 생성된 것을 볼 수 있다.

첫 번째 그룹(g)이 코드로 직접 생성한 것이고

(코드에 사용된 transform과 height값 280 (300-20)이 보인다.)

첫 번째 그룹 하위에 있는 그룹들은

D3에서 생성한 척도의 눈금(tick)이다.

눈금 그룹은 눈금(line)과 X축의 값(text)로 구성된 것을 볼 수 있다.


차트에 출력된 값을 보면 앞서의 예제와 다른 점이 있다.

숫자가 도형의 정중앙에 출력된다.

(이전 예제는 19, 29와 같은 두자리 숫자와 9와 같은 한자리 숫자의 출력 좌표가 안 맞다)

이전 예제는 도형의 중앙을 계산하기 어려웠지만

척도를 이용하여 간단하게 계산할 수 있다.

도형을 출력할 좌표(xScale)에 도형 크기(width)의 반(/2)을 더해주면

숫자가 찍힐 정확한 중앙 지점을 계산할 수 있다.

(위 코드에서 파란색으로 표시된 코드를 의미한다.)

SVG의 Text 테그의 text-anchor 스타일을 middle로 지정하면

지정된 좌표가 문자열의 중앙에 오도록 출력해 준다.


이번에는 다음 그림과 같이 Y축을 구현해 본다.

앞서의 코드에서 X축을 구현하기 위해

빨간색으로 표시된 코드와 같은 방식으로 구현하면 되니

직접 해본 후 진행하는 것이 좋다.

다음 코드를 보면 알 수 있듯이

앞서의 예제 코드에 빨간색으로 표시한 부분이

Y축을 보이기 위해 작성한 코드이다.

var svg = d3.select("svg");
var width  = parseInt(svg.style("width"), 10) -30;
var height = parseInt(svg.style("height"), 10)-20;
var svgG = svg.append("g")                                             
    .attr("transform", "translate(30, 0)");

var yScale = d3.scaleLinear()                                            ①
    .domain([0, d3.max(dataset, function(d){ return d.y; })])
    .range([height, 0]);  

  
svgG.selectAll("rect")
    .data(dataset)
    .enter().append("rect")
        .attr("class", "bar")
        .attr("height", function(d, i) {return height-yScale(d.y)})
        .attr("width", xScale.bandwidth())
        .attr("x", function(d, i) {return xScale(d.x)})
        .attr("y", function(d, i) {return yScale(d.y)});
svgG.selectAll("text")
    .data(dataset)
    .enter().append("text")
    .text(function(d) {return d.y})
        .attr("class", "text")
        .attr("x", function(d, i) {return xScale(d.x)+xScale.bandwidth()/2})
        .style("text-anchor", "middle")
        .attr("y", function(d, i) {return yScale(d.y) + 15});

   
svgG.append("g")                                                        
    .call(d3.axisLeft(yScale).ticks(5));

전체코드

① X축은 척도로 scaleBand를 사용한 반면,

Y축은 척도로 scaleLinear를 사용하였다.

X축의 값은 A, B, C 등의 문자열이었고,

Y축의 값은 1, 2, 3 등으로 연속된 숫자 값이다.

즉, 고정된 문자열에는 scaleBand, 일반 숫자값은 scaleLinear을 사용한다.

domain과 range는 모든 척도에서 같은 개념으로 사용된다.

다만, Y축이 숫자이기 때문에

0 부터 주어진 배열내 최대값(max(d.y))을 domain에 지정한다.


range 사용에서도 X축과 차이가 있다.

X축에서는 0부터 width로 지정했고

Y축에서는 height에서 0으로 지정했다.

즉, 큰 값이 먼저 나왔다.

X축은 A가 왼쪽, 즉 x좌표의 값이 가장 작다.

Y축은 1이 가장 하단, 즉 차트의 높이(Height) 값, 가장 큰 값을 가진다.

따라서, Y축은 domain 지정시 작은 값에서 큰 값으로

range 지정은 큰 값에서 작은 값으로 지정하여 서로 매핑 시켜준다.


② 마지막으로 X축은 axisBottom으로 하단에 축을 그리고

Y축은 axisLeft로 좌측에 축을 그린다.

다만, X축은 height값에 -20을 줘서 축의 위치를 바꾸어 줬지만

Y축은 다른 처리가 필요해서 축의 위치를 바꾸어 주지 않았다.

이것에 대한 자세한 설명은 뒤에 할 것이다.

Y축에는 척도의 개수(tick)을 5개로 지정하였다.

지정하지 않으면 D3에서 자동으로 지정한다.


③ 코드 작성이 완료 되었지만

이상의 코드에는 x축 생성할때 사용한 코드와 차이가 있다.

x축에 사용한 방식으로 Y축 코드를 작성하면

다음 표의 왼쪽처럼 작성해야 한다.

기대 되는 코드

  현재 작성된 코드

 svg.append("g")
    .attr("transform", "translate(30,0)")
    .call(d3.axisLeft(yScale).ticks(5));

 var svgG = svg.append("g")
     .attr("transform", "translate(30, 0)");
           
svgG.append("g")
    .call(d3.axisLeft(yScale).ticks(5));

Y축을 이렇게 작성하면 다음 그림에서 왼쪽 하단 처럼

X축과 섞여서 이상하게 보인다.

이 문제를 해결하기 위해

차트 전체를 구성하는 그룹을 만들고 (svgG = svg.append("g"))

그룹 자체를 오른쪽으로 30 px 정도 이동시켜준다 (translate(30, 0)).

그리고, 차트를 위해 생성되는 도형(rect), 라벨(text), 척도등을

이전에는 svg에 생성했으나 svgG에 생성한다.

앞서 작성된 코드를 보면

X축 예제는 svg에 생성하였고, Y축 예제는 svgG에 생성하였다.


척도와 관련된 구현을 마쳤다.

데이터 개수를 추가해서 실행해 보면

그림과 같이 도형(바)의 크기가 바뀌면서 실행된 것을 볼 수 있다.

var dataset = [{x:'A', y:9 }, {x:'B', y:19}, {x:'C', y:29}, {x:'D', y:39},
               {x:'E', y:29}, {x:'F', y:19}, {x:'G', y:9 }, {x:'H', y:29 }, {x:'I', y:39 }, {x:'J', y:49 }];


척도를 이용하므로서,

다양한 데이터에 유동적으로 반응할 수 있는 프로그램을 제작할 수 있게 된 것이다.


이번에는 그림처럼 차트에 그리드(Grid)을 추가한다.

그리드는 값의 정확한 위치를 보여 주기 위해 사용되는 것으로

D3에서는 간단하게 구현할 수 있다.

구현 방법은 코드에서 보듯이

그림과 같이 차트 도형(bar) 뒤편에

축을 그릴 때 사용한 눈금(tick)을 크게 그리는 것(tickSize)이다.

따라서, 눈금의 크기를 차트 영역의

너비(width)나 높이(height)만큼 크게 그리면 된다.

<style>
~~ 생략 ~~
.grid line {
  stroke: lightgrey;
  stroke-opacity: 0.7;
}
</style>
<script>
         
~~ 생략 ~~
   
svgG.append("g")           
    .attr("class", "grid")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale)
        .tickSize(-height)
    );
   
svgG.append("g")
    .attr("class", "grid")
    .call(d3.axisLeft(yScale)
        .ticks(5)
        .tickSize(-width)
    );      

  
~~ 생략 ~~

전체코드

X축의 그리드 선은

지정된 X좌표에 0부터 높이(height)까지 이어진 선이다.

따라서, X축의 그리드 선은 0부터 높이,

Y축의 그리드 선은 0부터 너비까지가 된다.

여기에 더 고려해야 할 것은 마이너스(-) 값이란 것이다.

X축의 경우 바닥(axisBottom)에 선을 그리기 때문

현재 그리는 기준이 바닥(height)이다.

즉, 바닥(height)이 0이되니, 0부터 –height까지 그려야 한다.

Y축은 왼쪽(axisLeft)을 기준으로 설정했기 때문에

축의 왼쪽은 플러스(+) 값, 오른쪽은 마이너스(-) 값으로 지정해서 그린다.


작성된 그리드 선에 대한 효과는 CSS로 지정한다 (.attr("class", "grid")).

CSS에서 선의 색상(stroke)을 회색(lightgrey)으로 지정하고

선의 투명도(stroke-opacity)를 70%(0.7)로 잘 안보이게 해서

차트 도형을 보는데 지장이 없게 했다.


그리드와 관련해서 마지막으로 정리 할 것은

그리드의 작성된 코드가 차트 도형을 생성하기 전에 있어야 한다.

그리드의 작성된 코드가 차트 도형보다 앞에 있어야

그림과 같이 선 위에 도형이 놓이게 된다.

(그리드가 배경처럼 보이게 된다)

반대의 경우에는 도형 위에 선이 놓여

선이 더 중요하게 강조되는 것처럼 보이게 된다.


문제: 앞의 그림을 보면 도형의 값(label)이 사라졌다.

다시 보이게 할 방법은 무엇일까?


마지막으로 구현해볼 기능은 툴팁(tooltip) 기능으로,

그림과 같이 차트 도형(바)에 마우스를 올리면

별도의 작은 창에 관련 데이터를 보여주는 기능을 구현한다.

이 기능을 구현하는 방법은

다음 문장을 그대로 코드로 작성하면 된다.

차트 도형에 마우스를 올리면(mouseover) 보이고,

도형에서 마우스가 벗어나면(mouseout) 안보이게 구현 한다.

마우스가 도형에 올라있는 동안은 마우스를 따라다니면서(mousemove)

마우스 근처에 작은 창(rect)을 하나 보여준다.

작은 창은 해당 도형의 데이터 값(Y 값)을 출력한다.

var barG = svgG.append("g");

barG.selectAll("rect")
    .data(dataset)
    .enter().append("rect")
        .attr("class", "bar")
        .attr("height", function(d, i) {return height-yScale(d.y)})
        .attr("width", xScale.bandwidth())
        .attr("x", function(d, i) {return xScale(d.x)})
        .attr("y", function(d, i) {return yScale(d.y)})
        .on("mouseover", function() { tooltip.style("display", null); })
        .on("mouseout",  function() { tooltip.style("display", "none"); })
        .on("mousemove", function(d) {
            var xP = d3.mouse(this)[0];
            var yP = d3.mouse(this)[1] - 25;
            tooltip.attr("transform", "translate(" + xP + "," + yP + ")");
            tooltip.select("text").text(d.y);
        });       
   
barG.selectAll("text");

var tooltip = svg.append("g")
    .attr("class", "tooltip")
    .style("display", "none");
   
tooltip.append("rect")
    .attr("width", 30)
    .attr("height", 20)
    .attr("fill", "white");
tooltip.append("text")
    .attr("x", 15)
    .attr("dy", "1em")
    .style("text-anchor", "middle");

전체코드

이 문장 다시 코드로 작성하면

툴팁은 rect(배경, 선등)와 text(값출력)로 구성된

tooltip이라는 SVG 그룹 태그로 (var tooltip = svg.append("g"))

평소에는 눈에 보이지 않는다 (display: "none").


이 tooltip은 도형의

mouseover 이벤트에서 display 값을 지워서(빈문자열) 보이게 하고

mouseout 이벤트에서 display 값을 none로 해서 보이지 않게 한다.

mousemove에서는 현재 마우스의 좌표(xP, yP)을 구해서

tooltip의 위치를 지정하고 값을 출력한다.

구해진 좌표값에 적절한 계산(-20)을 해서

툴팁이 마우스에 가려지지 않게 했다.


앞서 제시한 문제에서 도형의 값(label)이 사라진 문제를 제시하였다.

해결 방법은 위 코드에 사용된 barG 변수이다.

앞서의 예제에서는 svgG에 모든 기능을 구현했다.

이번 예에서는 그리드/척도(svgG)와

챠트 도형의 그룹(barG)을 다르게 작성했다.

하나의 그룹에 많은 개체가 있어서 위치 충돌이 있는 것으로 추측한다.

(정확한 이유를 아는 분은 댓글을)


이번에는 툴팁을 DIV로 구현해 본다.

개인적으로 rect와 같은 SVG 도형들이 덜 익숙한 것도 있고

보기 좋게 구현하는 것이 다소 어렵게 느껴져서

CSS로 간편하게 지정하는 DIV를 선호한다.

.toolTip {
    position: absolute;
    border: 0 none;
    border-radius: 4px 4px 4px 4px;
    background-color: white;
    padding: 5px;
    text-align: center;
    font-size: 11px;
}

~~ 생략 ~~
   
barG.selectAll("rect")
    .data(dataset)
    .enter().append("rect")
        .attr("class", "bar")
        .attr("height", function(d, i) {return height-yScale(d.y)})
        .attr("width", xScale.bandwidth())
        .attr("x", function(d, i) {return xScale(d.x)})
        .attr("y", function(d, i) {return yScale(d.y)})
        .on("mouseover", function() { tooltip.style("display", null); })
        .on("mouseout",  function() { tooltip.style("display", "none"); })
        .on("mousemove", function(d) {
            tooltip.style("left", (d3.event.pageX+10)+"px");
            tooltip.style("top",  (d3.event.pageY-10)+"px");
            tooltip.html(d.y);
        });       
   
barG.selectAll("text")

var tooltip = d3.select("body").append("div").attr("class", "toolTip").style("display", "none");

전체코드

다음 코드와 같이 D3의 append로 생성하고

toolTip이라는 CSS 클래스를 지정한다.

해당 클래스에서 CSS 속성으로 원하는 데로 지정하면 된다.

여기서는 툴팁 도형을 일반 사각(rect)이 아닌

모서리가 둥근 사각형으로 구현했다.

SVG Rect는 좌표(x, y)로 위치를 지정했지만

DIV는 left와 right 값으로 지정하고

위치 값도

SVG Rect는 SVG 내에서 위치로 지정했지만

DIV는 부모가 문서(body)이므로

문서 기준(pageX, pageY)으로 구현하였다.


문제

D3에서 척도는 매우 활용이 많고 중요한 클래스이다.

이 척도를 잘 이용하면 다양한 기능을 구현 할 수 있는데,

척도를 이용해서 각 도형의 색깔을 모두 다르게 부여하는 기능을 직접 구현해 본다.

전체코드


'JavaScript > Chart' 카테고리의 다른 글

1. D3 (SVG) 차트 만들기 - Bar I  (0) 2017.04.15
3. D3 (SVG) 차트 만들기 - Line  (6) 2017.04.15

이번에는 D3 (Data-Driven Documents)의 가장 큰 특징인

편리한 데이터 조작 관련 기능을

라인(Line) 차트를 만들면서 정리한다.


먼저, 라인 차트의 구현 방법을 알기 위해

이전에 작성한 바 차트 예제 중

하나를 골라서 마지막에 다음 코드를 추가한다.

var line = d3.line()
    .x(function(d) {return xScale(d.x); })
    .y(function(d) {return yScale(d.y); });
  
svg.append("path")
    .data([dataset])
    .attr("fill", "none")
    .attr("stroke", "blue")
    .attr("stroke-width", "1.5px")
    .attr("d", line);

전체코드

코드를 추가하고 실행해 보면

그림과 같이 라인 차트가 생성되는 것을 볼 수 있다.

즉, D3에서는 바 차트나 라인 차트를

간단하고 유사하게 생성하고 관리할 수 있다.


바 차트는 각각의 데이터에 대하여

각각의 도형(rect)를 생성하지만,

라인 차트는 각각의 선(데이터)이 이어져 하나의 선(Path)이 그려진다.

따라서 path를 생성하였다 (append).

그리고 각각의 선은 line 함수(인스턴스- d3.line())를 호출해서 그리게 된다.

라인 함수는 하나의 선 전체(path)에 대한 속성을 지정하는 부분과

각각의 선(line)에 대한 속성을 지정하는 부분으로 구성된다.


데이터(data) 사용법에도 차이가 있다.

바 차트에서는 data 지정시 배열을 그냥 넘겼다.

라인 차트에서는 data 지정시

배열(dataset)을 다시 배열([dataset])로 지정했다.

즉, 2차원 배열로 변환 한 것이다.

1 차원 배열 하나가 하나의 선(path)이 되기 때문이고,

2 차원으로 여러 개의 값을 지정하면 여러 개의 선이 생성된다.

라인은 여러 개의 선이 있다고 전제하고 있다.

2차원 배열을 기본으로 사용한다.


선의 색(stroke)은 파란색(blue)

선의 굵기(stroke-width)는 1.5px로 지정했다.

채우기(fill)은 없는 것(none)으로 했는데

채우기에 값(컬러)을 지정하면 영역 차트(Area Chart)가 된다.


그림을 보면 선의 시작 (A값) 부분이 축을 벗어나 있다.

바 차트는 Y축에서 일정 부분 떨어져서 시작하고

라인 차트는 Y축에 딱 붙어서 시작한다.

선의 위치(x)에 bandwidth를 추가해도 되지만

라인 차트 고유의 처리법으로 해결한다.


지금까지는 바 차트에 라인을 추가해서

간단하게 라인 차트와의 차이점을 살펴 봤다.

이제부터 그림과 같이 라인 차트에 맞는 예제를 구현한다.

또, D3의 특징인 데이터 사용법도 익히고

개인적으로 보다 직관적으로 이해 할 수 있다고 여겨

앞서 사용했던 Json(x,y)으로 구성된 1차원 배열을

2차원 배열로 구현하지 않고,

키와 값으로 구성한 Json 1차원 배열로 바꾸어서 진행한다.


사용할 데이터는 다음 표에서 작성한 것과 같다.

바 차트에서는 다음 표의 한 행이 사용되었던 것이고,

X: A, Y: 9와 같이 X, Y라는 키(key)로 구성된 Json을 이용했다.

이렇게 데이터를 구성해서 다음 표와 같은 데이터를 구성하려면

2차원 배열로 구현해야 한다.

(제시된 표 자체가 2차원 배열)

 

A

B

C

D

E

E

F

2016

9

19

29

39

29

19

9

2017

17

27

37

27

17

7

0


이것을 기존 방식으로 작성하면 다음과 같다.

var dataset = [
    [{x:'A', y: 9}, {x:'B', y:19}, {x:'C', y:29}, {x:'D', y:39}, {x:'E', y:29}, {x:'F', y:19}, {x:'G', y:9 }],
    [{x:'A', y:17}, {x:'B', y:27}, {x:'C', y:37}, {x:'D', y:27}, {x:'E', y:17}, {x:'F', y: 7}, {x:'G', y:0 }]
];


이러한 2차원보다 1차원이 이해하기 쉽고 구현하기 쉽기 때문에

하나의 행이 하나의 데이터(선-path)가 되게 하기 위해

A: 9, B: 19 와 같이

각 값을 Json으로 지정해서 1차원으로 구현했다 [라인 31].

(데이터 조작에 대한 연습을 위한 것도 있다.)

var dataset = [ {'A': 9, 'B':19, 'C':29, 'D':39, 'E':29, 'F':19, 'G':9},
                {'A':17, 'B':27, 'C':37, 'D':27, 'E':17, 'F':7, 'G':0} ];

배열의 원소 하나가 큰 Json으로 하나의 차원역할을 한다.

바 차트에서는 x, y라는 키가 고정인 것이고

라인 차트에서는 A, B, C등 키가 유동적인 차이가 있다.


표를 다시 확인해 보면 데이터 행이 2개이다.

즉, 두 개의 선이  생성되는 예제이다.

dataset 변수에 값만 추가해 주면

여러 개의 선이 계속 생성된다 [라인 31].

이렇게 여러 개의 선(행) 각각을 시리즈(series)라고 한다.

여기에서는 2016년과 2017년 두 개의 데이터(행),

두 개의 시리즈를 사용한다.


인터넷에서 구할 수 있는 예제들은

다음과 같이 시리즈를 데이터와 같이 구성하는데

여기서는 이해를 위해 별도의 배열 변수(series)를 사용했다 [라인 29].

var dataset = [ {'series': '2016', 'A': 9, 'B':19, 'C':29, 'D':39, 'E':29, 'F':19, 'G':9},
                {'series': '2017', 'A':17, 'B':27, 'C':37, 'D':27, 'E':17, 'F':7, 'G':0} ];

이렇게 데이터를 받아서

series 배열 변수로 분리 코드를 작성하는데

여기서는 미리 분리하고 작성하였다.


앞서 정리한 바 차트에 라인을 추가한 예제에서

라인은 2차원 배열을 기본으로 한다고 정리했다.

쉬운 이해와 개발을 위해

1차원 배열로 구현했기 때문에

이것을 2차원 배열로 변환하는 작업이 필요하다 [라인 34~39].

(처음부터 2차원으로 구현하는 것이 더 좋을 수도 있다.)


1차원의 dataset을 2차원의 data 변수로

변환하기에 앞서해 d3의 keys 함수를 이용하여

json key값들을 추출한다 [라인 34].

여기서는 각 시리즈의 데이터 개수가 같다는 전제로

첫 번째 (dataset[0]) 행의 json key값들을 추출하여

keys 변수에 저장했다.

(dataset[1]에 값이 더 있다면 출력되지 않는 버그가...)

이렇게 생성한 keys 변수는

일단 데이터 개수를 파악하기 위해서도 사용하고

X축에 척도를 출력하기 위해서도 사용한다 [라인 50].


다음 코드는 1차원으로 지정된 데이터(dataset)를

2차원의 데이터(data)로 변환하는 코드이다.

var keys = d3.keys(dataset[0]);
var data = [];

dataset.forEach(function(d, i) {
    data[i] = keys.map(function(key) { return {x: key, y: d[key]}; })
});

변환을 위해

dataset의 개수만큼(2회) 반복(forEach)한다 [라인 37].

그 안에서 각 배열의 원소인

Json의 개수만큼 반복(keys.map) 해서.

배열을 반환한다 [라인 38].

반환된 배열을 다시 배열 (data[i])에 넣으면서

2차원 배열이 만들어진다.

첫 배열이 만들어질 때,

바 차트 데이터와 같이

{A: 9}가 {x: ‘A’, y: 9}로 변환되어 저장된다 ({x: key, y: d[key]}).

바 차트 예제 데이터와 같이 진 것이다.


이렇게 구성한 데이터를

척도에 넣어주기만 하면 중요한 작업은 끝났다.

설명이 부족할 수도 있고,

어려울 수 있는 부분이지만

이 부분이 가장 중요한 부분이고

D3의 가장 큰 특징 중 하나라고 생각하니

꼭 이해하고 넘어가야 한다.


x축에 A, B, C ~~가 출력되도록

이 값을 가지고 있는 keys를 xScale에 데이터로 지정하였다 [50라인].

앞의 예제와 다르게

xScale에 scaleBand가 아닌 scalePoint가 사용되었다.

실행 결과로 짐작할 수 있겠지만,

이 둘은 고정된 값을 처리하는 공통점이 있지만,

scalePoint는 0부터 시작하는 차이가 있다.

즉, y축에 딱 붙어서 시작된다.

앞의 예에서 문제가 된 라인의 시작 위치가 해결된 것을 볼 수 있다.


Y축은 다소 복잡해 보이는데

데이터 변환과 유사한 방식으로 구현한 코드이다 [53라인].

D3의 max 함수는 주어진 배열내의 최대값을 찾아준다 [54라인].

첫 max에 dataset을 지정하면 2개의 json 데이터가 반환되고

다시 각각에 대하여 max를 keys 개수만큼 반복한다.

keys 개수만큼 반복하는 이유는

각 json 데이터 개수를 의미 하기 때문이다.

이렇게 반복해서 키에 해당하는 값(d[key])을 반환하고

이중에 가장 큰 값(max)이 반환되고,

반환된 2개의 큰 값 중 가장 큰 값이 반환되어

척도 구성을 위한 domain의 값으로 지정한다.

그리고 이 domain의 소수점이 너무 많으면

적당히 반올림한 값을 사용하도록 했다(nice).


이 부분이(54라인),

즉 데이터내의 최대값 찾기가

처음부터 2차원 데이터를 사용하지 않고

1차원 데이터를 사용한 이유 중의 하나이다.

얼마나 복잡한지 앞서 제시한 2차원 배열을 이용해서

이 부분을 구현해 보길 바란다.

(이런걸 해보는 것이 실력 향상의 지름길...)


여기서는 각 라인별로 다른 색을 사용했다 [라인 57, 85].

여기서 사용한 코드와 같이

D3에서 지정한 컬러 값 집합(schemeCategory20)을 사용해도 되고,

첫 예제에서 작성한 것처럼 각 컬러를 지정해도 된다.

(이것이 바챠트 마지막에 제시된 문제의 답이다.)

var colors = d3.scaleOrdinal(d3.schemeCategory20);
var colors = d3.scaleOrdinal().range(["red", "orange", "yellow", "green", "blue", "indigo", "violet"]);

D3에서 지정한 컬러 값에 대한 자세한 정보는

D3 문서페이지 맨 마지막에 있다.


여기까지가 라인 차트를 구현하기 위해

필요한 코드들이고,

나머지는 차트를 보기 좋게 하기 위한 코드들이다.

먼저, CSS를 이용해서 각 선에 마우스를 올리면(over)

선을 강조하기 위해 더 두껍게 (3px)

보이도록 작성했다 [라인 15].


41 ~ 47 라인 코드는

SVG내에서 차트를 중앙에 놓기 위한 코드이다.

var margin = {left: 20, top: 10, right: 10, bottom: 20};
var svg = d3.select("svg");
var width = parseInt(svg.style("width"), 10) - margin.left - margin.right;
var height = parseInt(svg.style("height"), 10)- margin.top - margin.bottom;

var svgG = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

즉, 상하좌우에 여백(margin)을 주는 코드이다.

왼쪽(left)과 상단(top)은 주어진 값만큼

전체 구조를 이동(transform)시키면 된다.

우측(right)과 바닥의 여백(bottom)은

차트를 생성할 때 빼고 계산하면 된다.

계산하는 것이 척도이니

X축 계산에 사용되는 width 값을

여백 만큼 (-margin.left - margin.right) 줄여주고

Y축 계산에 사용되는 height 값을

여백 만큼(-margin.top - margin.bottom) 줄여주면 된다.


마지막으로 범례(legend)를 구현한다 (라인 88~).

범례는 차트 내에서 적당한 위치(우측 상단)에

각 라인이 나타내는 의미를 보여 준다.

(위치는 지정하기 나름이다.)

각 라인은 색으로 구분하기 때문에

도형(rect)을 생성하고,

어떤 데이터 인지(series)를

문자로(text)로 출력해 주면 된다.


마지막 예제로 그림과 같이 툴팁 기능을 구현한다.


구현할 툴팁 기능은

바 차트에서는 도형(rect)에 마우스를 올리면

해당하는 값을 보이게 했다.

라인 차트는 하나의 선이기 때문에 이렇게 구현할 수 없다.

대체 방법은 X축의 값이 있는 부분에

적당한 도형(rect, circle등)을 생성하고(append)

이 도형에 마우스를 올리면 툴팁을 보이도록 작성한다.

반지름(r)이 3 px인 원을 생성하였다.

보이고 숨기는 방식은 바 차트와 동일하다.

코드를 복사해서 그대로 사용했다.


<style>
.toolTip {
    position: absolute;
    border: 1px solid;
    border-radius: 4px 4px 4px 4px;
    background-color: yellow;
    padding: 5px;
    text-align: center;
    font-size: 11px;
    min-width: 30px;
}
</style>

var line = d3.line()
    //.curve(d3.curveBasis)
    .x(function(d) { return xScale(d.x); })
    .y(function(d) { return yScale(d.y); });


var lineG = svgG.append("g")
    .selectAll("g")
    .data(data)
    .enter().append("g");

lineG.append("path")
    .attr("class", "lineChart")
    .style("stroke", function(d, i) { return colors( series[i]); })
    .attr("d", function(d, i) {return line(d); });

lineG.selectAll("dot")   
    .data(function(d) {return d })
    .enter().append("circle")                               
        .attr("r", 3)       
        .attr("cx", function(d) { return xScale(d.x) })        
        .attr("cy", function(d) { return yScale(d.y);})   
        .on("mouseover", function() { tooltip.style("display", null); })
        .on("mouseout",  function() { tooltip.style("display", "none"); })
        .on("mousemove", function(d) {
            tooltip.style("left", (d3.event.pageX+10)+"px");
            tooltip.style("top",  (d3.event.pageY-10)+"px");
            tooltip.html(d.x + "<br/>" + d.y);
        });   
       
var tooltip = d3.select("body").append("div").attr("class", "toolTip").style("display", "none");

전체코드

다만, 앞서 제작한 두 번째 예제는

값에 따라 휘어지는 라인이 부드럽다 (curveBasis).

이렇게 부드러운 상태에서

정확한 값에 도형(dot)을 출력하면

선과 도형의 위치가 맞지 않게 된다.

따라서 선도 그림과 같이 딱 맞추어서 출력되게 하기 위해

부드러운 선이 아닌 꺾인선으로 구현해야 한다.

.curve(d3.curveBasis)를 지우면

기본 설정이 적용되어

그림과 같이 꺾인 선으로 출력된다.


이상으로 라인 챠트와 관련된 내용을 정리했지만

실제로는 데이터를 다루는 방법에 대하여 강조하였다.

이유는 D3의 특징이기도 하고

이 데이터 처리 방식(위 예제에서)에

다시 rect로 도형을 생성하면 Grouped bar 차트가 되기 때문이다.

즉, 데이터 처리와 구성이 중요하다는 의미이다.

따로 정리하지 않지만

이상의 예제를 다시 Grouped bar차트로 구현한 소스를 공유한다.

     groupedBar1.html
     groupedBar2.html
     groupedBar3.html
     groupedBar4.html




앞서 사용된 데이터가 다음 표와 같았다.

 

A

B

C

D

E

E

F

2016

9

19

29

39

29

19

9

2017

17

27

37

27

17

7

0


이것을 여기서는  다음과 같은 데이터로 구성해서 코드를 작성했다.

var dataset = [ {'A': 9, 'B':19, 'C':29, 'D':39, 'E':29, 'F':19, 'G':9},
                 {'A':17, 'B':27, 'C':37, 'D':27, 'E':17, 'F':7, 'G':0} ];

하지만 다음과 같이 지정하는 방법이

더 직관적이고, 많이 사용하는 방법(예 c3js)이다.

이렇게 구현해 보면 실력향상에 도움이 될 것이다.

var dataset = [ {'A', 'B', 'C', 'D', 'E', 'F', 'G'},
                { 9,  19,  29,  39,  29,  19,  9},
                {17,  27,  37,  27,  17,   7,  0} ];




'JavaScript > Chart' 카테고리의 다른 글

1. D3 (SVG) 차트 만들기 - Bar I  (0) 2017.04.15
2. D3 (SVG) 차트 만들기 - Bar II  (0) 2017.04.15

+ Recent posts