테트리스 게임을 두 가지 방법으로 개발하면서

하나의 제품이나 기능을 다양하게 개발하는 방법을

다음과 같이 단계별로 정리하였다.

1. 개요

2. 배열 방식의 도형 회전

3. Bitmask 방식의 도형 회전

4. 배열기반 회전 방식의 테트리스

5. Bitmask기반 회전 방식의 테트리스

6. requestAnimationFrame


Bitmask를 사용한 예제에서는

시간의 흐름을 setTimeout() 함수나 setInterval() 함수가 아닌

requestAnimationFrame() 함수를 사용하였다.


setTimeout() 함수나 setInterval() 함수는 다음과 같은 문제가 있다.


setInterval() 함수는 언제나 콜백함수를 호출하기 때문에 브라우저의 다른 탭이 선택된 경우와 같이 실제 화면을 다시 그릴 필요가 없는 경우에도 화면을 다시 그린다. 그래서 시스템의 리소스 낭비를 초래하여, 불필요한 전력을 소모하게 만든다. 또한, 디스플레이 갱신 전에 캔버스를 여러 번 고치더라도 디스플레이 갱신 바로 직전의 캔버스 상태만 적용이 되기 때문에, 프레임 손실이 발생할 수도 있다.

출처: Beautiful Code


게임과 같은 처리에는 requestAnimationFrame() 가 더 적합하다는 의미로

IE 10 이후나 Firefox, Chrome등에서 사용할 수 있다.


다음 예제는 MSDN에서 제공하는 예제

개인적으로 가장 쉬운 예제인 것 같아서 이 코드를 기본으로 정리하였다.

예제를 실행하고, "Start" 버튼을 누르면

빨간색의 도형이 왼쪽에서 오른쪽으로 천천이 이동한다.


먼저, 39 라인에서 requestAnimationFrame()을 새로 정의하였다.

재정의한 것은 IE, FireFox, Chrome등의 웹브라우저에 맞추어

적절한 requestAnimationFrame()을 사용하기 위한 것으로 [라인 40~43]

requestAnimationFrame()을 지원하지 않는 브라우저일 경우에는

기존과 같이 setTimeout()을 사용하도록 하였다 [라인 44~46].

setTimeout()의 실행 간격은 1000 / 60 (=16.667)로

이 값은 밀리 세컨드이니 0.0167초에 한번씩 실행된다.

이렇게 지정한 이유는 requestAnimationFrame() 함수가

0.0167초에 한번씩 실행되도록 실행 시간이 고정되어 있다.


실행을 중지하는 cancelAnimationFrame()도

requestAnimationFrame()와 동일한 방식으로 작성되었다 [라인 49~57].


실행 화면에서 “Start” 버튼을 누르면

Start()함수가 호출되고 [라인 30]

다시 requestAnimationFrame(requestAFrame)가 호출되면서

render()가 콜백 함수로 실행된다 [라인 31].


render() 함수에서는

화면에 있는 빨간 DIV의 왼쪽(left) 값을 3px씩 증가시켜서

왼쪽에서 오른쪽으로 움직이게 한다 [라인 26].


setTimeout()과 requestAnimationFrame()의 차이를 보기 위해서는

 “Start” 버튼을 눌러서 실행을 시킨 뒤에

웹 브라우저를 작게(Minimize) 한 뒤에 복원을 시켜보면 된다.

도형의 이동이 웹 브라우저를 작게 했을 때의 위치에서 멈추었다가

복원할 때 다시 해당 위치에서 시작되는 것을 볼 수 있다.


이 코드를 테트리스에 적용하는 것은 문제가 있다.

기존에 작성한 코드는 0.4 (400 / 1000) 초에 한번씩 실행되는데

requestAnimationFrame()는 무조건 0.0167 초마다 실행된다.

지나치게 빨리 실행되어,

도형들이 빨리 떨어지게 된다.


requestAnimationFrame()는 실행 시간을 지정할 수 없기 때문에

약간의 트릭이 필요하고,

이 방법이 Bitmask 참고 예제에 잘 구현되어 있다.

다음 코드는 이 예제에서 추출한 코드로,

requestAnimationFrame()을 지정된 시간 간격으로 실행되게 구현한다.

function render() {
    elm.style.left = ((lpos += 3) % 600) + "px";
                                             
}
var dt = 0, step = 0.4;
var last = now = timestamp();
function timestamp() { return new Date().getTime(); }
   
function frame() {
    now = timestamp();
    update(Math.min(1, (now - last) / 1000.0));
    last = now;
    requestId = window.requestAFrame(frame);
}
function update(idt) {
    dt = dt + idt;
    if (dt > step) {
        dt = dt - step;
        render();
    }
}
   
function speedUp() {
    step -= 0.05
    if (step < 0.1) step = 0.1;
}
   
function start() {
    frame()
}

전체코드

수정/추가된 코드가 많아 보이지만 원리는 간단하다.

현재 시간과 과거 시간의 차이를 계산해서

차이가 지정된 시간 (0.4초)보다 크면

도형의 위치를 이동시켜 준다.


코드와 연결해서 정리하면

현재 시간(now)과 과거 시간(last)의 차이(now - last)를 계산해서(dt)

차이가 지정된 시간 (0.4초)보다 크면 (dt > step)

도형의 위치를 이동시켜 준다 (render()).


조금 더 자세하게 정리하면,

현재 시간은 현재 호출된 시간(now)을 의미하며

과거 시간은 마지막으로 호출된 시간(이전 now)을 의미한다.

현재 시간과 과거 시간의 차이(idt)는 거의 항상 0.0167 초 이다.

(밀리 세컨드이기 때문에 1000 으로 나누어 준다. (now - last) / 1000)

requestAnimationFrame() 함수가 실행되는 시간이기 때문이다.


이 값을 계속 누적해서 (dt = dt + idt) 경과 시간을 계산하게 되고

경과 시간(dt)이 지정된 시간 (0.4 : step)보다 크면 (dt > step)

도형의 위치를 이동시켜 준다 (render()).

즉, 경과시간(dt)은 지정된 시간(0.4 초)이 될때까지 0.0167 초씩 증가한다.

지정된 시간이 되면 도형을 이동 시키고, 경과 시간을 초기화 한다 (dt - step).

초기화 방법이 0 이 아닌 dt – step 인 것은

미세한 시간 차이라도 처리해 주기 위한 것이다.


기존 코드에 추가한 이번 예제는 속도 조절 기능도 구현했다.

HTML 버튼을 추가하고,

이 버튼을 클릭하면 speedUp() 함수를 호출하도록 작성했다.

speedUp() 함수에서는 지정된 시간(step)을 0.05초씩 작게(-) 해서

0.4초 마다 호출되던 것을 0.1 초까지 빨라지도록 했다.

이것은 테트리스의 난이도 조절에 사용할 수 있다.


이상의 코드를 앞서 정리한

Btmask 기반 회전 방식을 토대로 제작한 마지막 예제에 적용한다.


requestAnimationFrame() 예제에 추가한(두번째 예제) 코드를

Btmask 마지막 예제에 그대로 추가해 주면

requestAnimationFrame()를 사용하여

게임 난이도를 조절하는 예제를 구현할 수 있다.

function update(idt) {
    dt = dt + idt;
    if (dt > step) {
        dt = dt - step;
        playingTetris();
    }
}
window.onload = function(){
    requestId = window.requestAFrame(frame);
}

전체코드

추가 후 바꾸어 줄 코드는 이상의 코드(빨간색)과 같이

setInterval() 대신 requestAnimationFrame(),

도형을 이동시키는 rander() 대신

테트리스를 실행하는 playingTetris()을 호출하면 된다.


게임을 실행한 후,

한 행을 채우면 갑자기 도형들이 빨라지는 것을 볼 수 있다.



지금까지 테트리스를 제작하는 두 가지 방법을 정리하였다.

두가지 제작 방법으로 작성된 참고 예제에서

쉽고 좋다고 여겨지는 코드를 추출하여 정리하였다.

두 예제 모두 비효율적인(?) 코드와 재미난 코드로 구성되어 있어

배울점이 많으니 더 살펴 보길 바란다.

작성한 예제 코드에 적절한 난이도(시간) 조절과

다음 도형을 볼 수 있는 미리보기,

게임 진행에 따른 점수 기능을 구현하면

제법 그럴 듯한 테트리스가 될 것이다.

(참고 예제에 구현되어 있다)

어렵지 않으니 각자 구현해 보길 바라고 댓글로 공유하면 아무 멋진 일이 될 것 같다.




테이블 정렬(table sorting)

자바 스크립트 기본 문법을 이해하기 위한 예제였고

테이블 컬럼 이동 (Drag and Drop Table Columns) 예제를 구현하면서

자바 스크립트의 이벤트(마우스) 처리와 CSS 이용에 대한 방법을 정리한다.

테이블 컬럼 이동은

특정 컬럼(A)을 마우스 버튼을 누른 체 움직여서 (Drag)

다른 컬럼(B)에서 멈출 경우 (Drop)

특정 컬럼(A)을 해당 컬럼(B)의 앞으로 이동시키는 것을 의미한다.

개발 진행 과정은 이해를 쉽게 하기 위해

다음과 같이 단계별로 진행한다.

  1. 마우스 이벤트 기본 구현
  2. CSS 클래스 문제 해결
  3. 컬럼 이동(Drag and Drop) 처리
  4. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 헤드 보기
  5. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 전체 보기
  6. JQuery


예제 코드는 다음 사이트의 코드를

단계별로 풀어서 쉬운 부문만 정리하였으니

실제로 사용할 경우에는

다음 사이트의 코드를 활용하거나

dragtable같은 라이브러리를 사용하는 것이 좋다.

JQuery의 Sortable 예제도(웹페이지 중간의 Sort tables)

좋은 참고가 될 수 있다.


테이블 컬럼 이동 (Drag and Drop)은

4가지 마우스 액션에 의해 이루어 진다.

먼저, 이동하려는 컬럼의 헤드를 마우스로 누르고(Mouse Down)

이동할 위치의 컬럼으로 마우스를 움직여서(Mouse Move)

클릭한 마우스 버튼을 놓으면 (Mouse Up)

이동하려는 컬럼을 이동할 컬럼의 앞으로 옮겨준다.

이때, 이동하려는 컬럼과 이동할 위치의 컬럼을

CSS를 이용하여 구별해주는 것이 좋다.

특히, 이동할 위치의 컬럼으로

마우스가 올라오면(Mouse Over) 배경색을 지정하고,

다른 컬럼으로 이동하면

즉, 마우스가 현재 컬럼에서 떠나면(Mouse Out) 원상태로 되돌린다.

따라서 Mouse Move 이벤트는

Mouse Over와 Out으로 구분하여 처리한다.


이상의 개념으로 직접 구현해 본 후,

다음 코드를 보는 것이

실력 향상에 도움이 되니 시도해 보길 바란다.


위 코드에서 CSS 부분은 [라인 5 ~25]

Table 태그에 대한 스타일과

이동하려는 헤드(TH)의 CSS 클래스(dragging - 라인 16)와

이동한 새로운 위치의 CSS 클래스(hovering - 라인 21)이다.


다음으로 자바 스크립트에서는

문서가 로드 되고(onload) 나면 [라인 28]

모든 헤드(TH)를 찾아서 (getElementsByTagName – 라인 29)

사용할 마우스 이벤트에 이벤트 핸들러들을 지정한다.

여기서 컬럼의 헤드로 TH 태그를 사용했지만

TH 태그를 사용하지 않고 TD 태그를 사용할 경우에는

첫 번째 행(TR)의 TD가 헤드가 된다.


웹페이지에서 마우스를 누르고 움직이면(드래그하면)

기본적으로 해당 범위 안에 있는 내용들이 선택(select)된다.

여기서는 드래그하면 컬럼을 이동시킬 것이기 때문에

내용이 선택되지 못하게(return false)

onselectstart 이벤트에서 처리해 준다 [라인 31].


마지막 부분은 HTML 부분으로

테이블(Table) 태그를 생성하고

3개의 행(TR)과 10개의 컬럼(TH, TD)을 생성하였다 [라인 64~].


마우스 이벤트에 대해서 좀더 상세하게 정리하면

특정 컬럼을 마우스로 누르면(MouseDown – 라인 40),

현재 눌려진 컬럼(this)이 이동할 컬럼이라고 여겨

dragTD 변수에 넣어서 보관한다 [라인 41].

그리고 시각적인 표시를 위해

이 컬럼에 CSS dragging 클래스를 지정한다. [라인 42]

이 컬럼은 다른 컬럼보다 조금 어둡게(background:#eee) 처리한다.


이동할 대상 컬럼의 헤드에 마우스가 올라오면(Over – 라인 45)

Hovering 클래스를 지정해서

다른 색상(background:#ccc)으로 표시한다 [라인 47].


이 컬럼에서 마우스가 떠나면(Out – 라인 - 50)

Hovering 클래스를 지정해서 배경색을 바꾸었던 것을

Hovering 클래스를 지워서 원상태로 돌린다 [라인 52].


옮기려는 컬럼에서

눌렀던 마우스 버튼을 놓으면 (Mouse Up – 라인 55)

현재 컬럼에 지정된 Hovering 클래스를 지우고 [라인 56]

드레그 하던 컬럼(dragTD)에 지정된

dragging 클래스를 지워주고 [라인 57]

드레그 하는 필드를 초기화 한다 [라인 58].

컬럼에 마우스가 오거나(over), 떠나면(out)

CSS 클래스를 이용하여

배경색을 지정하거나 제거하게 되는데,

마우스가 눌러진 상태에서만 작동하고

그냥 마우스가 올라오거나 떠났을 때는 아무 것도 하지 않아야 한다.

즉, 마우스가 눌러진 상태를 확인하기 위해

dragTD 변수의 값이 있는지 확인한다 [라인 46, 51].

dragTD 변수의 값은 마우스 버튼이 눌러졌을 때는

해당 컬럼의 헤드를 가지고 있고 [라인41],

놓았을 때는(Up) null을 가지고 있다 [라인58].


마우스 이벤트와 관련된 상세한 정보는 자료를 찾아보기 바라고

여기에서는 사용하지 않았지만

각 이벤트 핸들러에서 파라미터로 사용된 ev에 대해서 정리한다.

ev 변수는 Event 객체를 나타내는 것으로

현재 발생한 이벤트의 각종 정보를 가지고 있다.

클릭 이벤트의 경우 클릭된 개체를

event.target (ev.target)으로 알 수 있다.

여기서는 this를 사용하였다.

이외에 클릭된 위치 좌표나,

클릭된 마우스 버튼(왼쪽, 오른쪽)등의 정보를 알 수 있다.


마우스 이벤트 외에도 많은 이벤트 들이 있으니

잘 익혀두어야 하고,

이상의 마우스 동작은 마우스를 이용하여 움직이는

대부분의 프로그램에서 사용되는 방식이니

잘 이해해 두는 것이 좋다.

(페이지 마지막에 있는 예제 참조)


정리하면,

마우스 버튼을 누르면(down)

해당 개체 정보를 보관하고 표시를 한다.

마우스 버튼을 놓으면(up)

보관된 정보를 초기화하고

작업 대상으로 표시한 것을 원래대로 되돌린다.

작업 대상이 있을 경우에만,

마우스의 움직임(Move-over, out)에 따라

필요한 행동을 하도록 작성한다.



두 번째 구현으로

테이블 헤드(TH)의 배경색(background:#aaa)을 지정해 본다.

기존 코드의 HTML에서 Style로 지정해도 되지만

다음과 같이 tableHead라는 클래스를 추가해서 테스트 한다.

.tableHead{
    background:#aaa;
    cursor:pointer
}
</style>   
<script>
window.onload = function() {
    var head = document.getElementsByTagName("th");
    for (i=0; i<head.length; i++) {
        head[i].onselectstart = function() { return false }
        head[i].onmousedown = mousedown;
        head[i].onmouseover = mouseover;
        head[i].onmouseout = mouseout;
        head[i].onmouseup   = mouseup;
        head[i].className = "tableHead";
    }

}


이렇게 추가하고

드레그를 해보면 버그가 발생하는 것을 알 수 있다.

tableHead에 의해 배경색이 어두운 색으로 지정되었는데,

드레그를 하고 난 뒤에

배경색들이 모두 사라지는 것을 볼 수 있다.

이 버그를 해결한다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
}

function mouseover(ev){
    if (dragTD === null) { return;}
    addClass(this, "hovering");
}

function mouseout(ev){
    if (dragTD === null) { return;}
    removeClass(this, "hovering");
}

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
    dragTD = null;
}

function addClass(src, classname) {
    if (src.className.indexOf(classname) === -1 ) {
        src.className += " " + classname;
    }
}

function removeClass(src, classname) {
    src.className = src.className.replace(" " + classname, "");
}

전체 코드

버그가 발생한 것은

CSS 클래스를 사용할 때

className에 값을 바로 지정했기 때문에 생긴 것이다.

테이블 헤더에 CSS 클래스가 없을 때는

다른 CSS 클래스를 넣고 빼도 (className = "") 문제가 없었다.

하지만 head[i].className = "tableHead"로

이미 CSS 클래스가 지정되어 있는데

this.className = "dragging"나 "hovering"으로 지정하면

기존 것이 지워지고 새로운 CSS 클래스가 설정된다.

초기화 하기 위해 className = "" 로 지정하면

기존 CSS 클래스가 지워지게 된다.

기존에 있던 tableHead를

추가해 주는 것이 방법이 될 수 있겠지만,

여러 개가 사용되면 다시 문제가 생기기 때문에 좋지 않다.

웹 개발시 많은 CSS 클래스가 사용되는데

매번 추가해주는 작업을 할 수 없다.


일반적으로 많이 사용하는 방식은 다음과 같다.

         className += " " + classname

대입(=)하는 것이 아니고,

문자열을 추가(+)하는 방식으로 사용한다.

다만, 기존에 있는 클래스명과 연결되는 것을 막기 위해

공백 하나(" ")를 추가해 준다.

즉, className속성에 "A B C D ..." 으로 이어지게 구성한다.

반대로 CSS 클래스를 제거할 때는

className.replace(" " + classname, "")와 같이

해당 클래스 이름을 찾아서 공백으로 바꾸어(replace) 준다.

이것을 좀더 쉽게 사용하기 위해

addClass(), removeClass()로 함수화 해서 사용했다.


세번째로 실제 컬럼 이동(Drag and Drop) 을 구현해 본다.

마우스 버튼을 누른 상태에서 이동한 후

마우스 버튼을 놓으면 (up)

눌렀을 때의 컬럼(dragTD)을

놓았을 때의 컬럼 앞으로 옮겨주면 된다.

컬럼(열)을 이동하는 방법은

앞서 행을 정렬하면서 사용한 것처럼

insertBefore 함수를 사용하지만

구현이 조금 더 어렵다.


테이블 태그는

하나의 행에 모든 컬럼의 정보가 있기 때문에

테이블 정렬에서는 하나의 행(row)을 이동시켰다.

하지만 컬럼을 이동하는 것은

하나의 컬럼에 모든 행의 정보가 있는 것이 아니기 때문에

처리가 복잡해 진다.

컬럼의 정보는 행에만 있기 때문이다.

즉, 컬럼을 이동시키려면

모든 행의 해당 컬럼을 찾아서 이동시켜야 한다.

따라서 이동시킬 컬럼을 찾기 위해

행 내의 컬럼의 위치 정보를 이용한다.

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
   
    var srcInx = dragTD.cellIndex;
    var tarInx = this.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        tds = rows[x].cells;
        rows[x].insertBefore(tds[srcInx], tds[tarInx])
    }
   
    dragTD = null;
}

전체 코드

컬럼의 위치를 나타내는 속성은 cellIndex이다.

즉, 이동시키고자 하는 컬럼을 cellIndex로 찾고 (srcInx)

이동할 위치의 컬럼을 cellIndex로 찾아서 (tarInx)

insertBefore로 옮겨 주면 된다.

         rows[x].insertBefore(tds[srcInx], tds[tarInx])

이것을 모든 행에 대해서 진행해야 하기 때문에

행의 개수(table.rows.length) 만큼 반복한다.


지금까지는 컬럼을 드레그 할 때

선택한 컬럼의 배경색을 바꾸도록 표시하였다.

네번째로 좀더 쉽고 편히 볼 수 있도록

이동하는 마우스 옆에

현재 선택된 컬럼의 내용이 나타나도록 구현한다.

구현 방법은

마우스가 움직일 때 마다(MouseMove)

커서의 좌표를 구해서

컬럼의 내용을 보여준 개체(div)에 지정한다.

<style>
.draggedDiv {
    width:auto;
    height:auto;
    padding:2px 8px;
    border:1px solid #000;
    position:absolute;
    background:#eee;
}
</style>   
<script>
window.onload = function() {
    ~~ 생략 ~~
    document.documentElement.onmouseup = documentMouseup;
    document.documentElement.onmousemove = documentMouseMove;
}
function documentMouseup(ev){
    if (dragTD) {
        removeClass(dragTD, "dragging");
        dragTD = null;
        draggedDiv.parentNode.removeChild(draggedDiv);
        draggedDiv = null;   
    }
}
function documentMouseMove(ev){
    if (!draggedDiv) { return;}
   
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
}
var dragTD = null, draggedDiv=null;
function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.innerHTML = this.innerHTML;
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
}
function mouseup(ev){
    draggedDiv.parentNode.removeChild(draggedDiv);
    draggedDiv = null;
    ~~ 생략 ~~
}

전체코드

이동하려는 컬럼을

별도의 창에서 보여주기 위해서

별도의 창을 동적으로 생성하고 삭제하도록 구현했다.

별도 창(DIV)을 생성하는 것은 (createElement("div"))

이동할 컬럼을 선택하는 시점

즉, 마우스 버튼이 눌려질 때이다 (Mouse Down).

삭제(removeChild)되는 시점은

이동하려는 위치(컬럼)을 선택한 시점

즉, 눌려진 마우스 버튼을 놓았을 때이다 (Mouse Up).

이상의 코드를 확인해 보면

전역으로 선언된 draggedDiv 변수에

mousedown, mouseup 함수에서

div를 생성하고 삭제하는 것을 확인 할 수 있다.


onload 이벤트에서 별도 창을 생성하고

mousedown, mouseup 함수에서

보이거나 숨기는 방식으로 구현 할 수도 있으니 직접 구현해 보길 바란다.


별도 창을 생성한 뒤,

이동할 컬럼의 내용(this.innerHTML)을

별도 창에 넣어주고(draggedDiv.innerHTML),

마우스의 움직임(Mouse Move)에 따라서

별도 창의 좌표(left, top)을 바꾸어 준다.


컬럼의 이동은 테이블의 헤드 컬럼으로 제한을 하기 때문에

모든 이벤트를 테이블 헤드에 대하여 적용하였다.

하지만, 별도 창은 마우스가

테이블 태그에 있거나 웹 페이지에 있으나

마우스를 누르고 움직일 때는 항상 보여야 하기 때문에

문서(document)의 Mouse Move 이벤트에 적용한다 (documentMouseMove).

Event (ev) 클래스에서 제공하는

pageX, pageY 속성을 통해서

현재 마우스 커서의 위치 값을 찾을 수 있다.


문서(document)의 Mouse Up 이벤트를 사용한 것은 (documentMouseup)

컬럼을 이동하기 위해서 마우스 버튼을 누른 상태에서

테이블 태그의 헤더가 아닌

다른 태그나 웹 페이지에서

마우스 버튼을 놓았을 경우를 처리하기 위한 것이다.

이 경우 기존의 mouseup()에서 작성한

드래그 종료(취소) 코드와 동일하게 작성한다.

이동하겠다고 한 컬럼에 적용된

CSS클래스(dragging)를 제거하고

이동 컬럼(dragTD) 변수에 Null을 지정하여

다른 이벤트(over, out)이 작동하지 않게 한다.


마지막 구현으로 별도 창에 컬럼의 헤드뿐 아니라

컬럼 전체의 내용이 나오게 구현해 본다.

하나의 컬럼에 10개의 행이 있다면 모두 나타나도록 구현한다.


구현 방법은 네 번째로 구현한 것과 동일하다.

다만, 컬럼의 헤드 내용을 별도 창에 넣어주었던 것을

테이블 태그를 생성하여

모든 행의 해당 컬럼을 복사해서 넣어준다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
   
    var dragTable = document.createElement("table")
    draggedDiv.appendChild(dragTable);
    var srcInx = dragTD.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        var tr = rows[x].cloneNode(false);
        dragTable.appendChild(tr);
        var tds = rows[x].cells[srcInx].cloneNode(true);
        tr.appendChild(tds);
    }   
}

전체코드

먼저, 별도 창에 새로운 테이블 태그를

생성하여 추가한다.

그리고, 이동하기 위해 선택한 컬럼의 위치(cellIndex)를 파악하고(srcInx),

기존 테이블의 행 정보를 가지고 와서

해당 컬럼(srcInx)의 위치(cellIndex)에 있는

컬럼만 복제한다(cloneNode).


모든 행을 복제해야 하기 때문에

행의 개수(rows.length)만큼 반복하고,

각 행(TR - rows[x])을 먼저 복제한다.

새로운 테이블에 행이 없기 때문에

행을 먼저 만들어야 열을 추가할 수 있다.

행을 복제하고

행의 이동할 컬럼만(rows[x].cells[srcInx])만 복제해서 추가한다.


새로 생성하지 않고 복제를 하는 이유는

각 행과 열(컬럼)이 가지는 정보(내용, CSS등)들을

그대로 복제해야 실제 테이블에서 보이는 것과

별도 창에서 보이는 것이 같아 지기 때문이다.


복제에 사용한 cloneNode 메소드의 파라이터로

행을 복제할 때는 false를

열을 복제할 때는 true를 지정했다.

false는 자식 정보를 복제하지 않겠다는 것을 의미하는 것으로

행을 복제할 때 자식 정보를 복제하면

모든 열(컬럼)이 복제되기 때문이다.

행의 스타일만 복제하고

열은 지정된 열(srcInx)에 대한 모든 것을 복제한다.


JQuery 변환 방법에 대해서는

테이블 정렬(table Sorting)등에서 정리했기 때문에

별도로 정리하지 않는다.

다만, 앞서 두 번째 구현에서

정리한 addClass(), removeClass() 함수는 사용하지 않는다.

동일한 이름의 동일한 기능이 JQuery에서 제공되고 있다.

함수가 아니라 메소드로 사용하는 차이가 있다.

    AAA.addClass("dragging")
    AAA.removeClass("dragging")

전체코드



응용

이상의 MouseDown, MouseMove, MouseUp 등의

마우스 이벤트는 다양하게 활용될 수 있다.


조금 작은 예제로 팝업 윈도우를 구현 할 수 있다.

최근의 웹 개발에서는 팝업 윈도우를 DIV로 구현한다.

그림과 같이 팝업 윈도우의 헤드를

마우스를 누르고, 움직이고, 놓으면

해당 팝업 윈도우가 따라 움직이도록 구현한다.


전체 코드

좀 큰 예제는 파워포인트를 구현하는 것이다.

파워포인트의 슬라이드 위에서 

마우스를 누르고, 움직이고, 놓으면

움직인 만큼을 크기로 하는 도형이 생성된다.

다시 해당 도형을

마우스를 누르고, 움직이고, 놓으면

놓은 위치로 도형을 이동시켜 준다.

이러한 기능을 잘 구현한 예제가 다음 주소에 있다.

(SVG를 사용한 것으로 구현 방법에 차이가 있다.)

https://jgraph.github.io/mxgraph/javascript/


단계 1: 마우스 드레그 1 -  좌에서 우로

단계 2: 마우스 드레그 2 - 우에서 좌로 추가

단계 3: 도형 생성

단계 4: 도형 이동



테이블 태그의 데이터를 정렬(table sorting)하는 기능을 구현하면서

자바 스크립트의 기본 문법과 Jquery 사용법을 익힐 수 있도록 정리하였다.

기초 학습을 위해 정리하는 것으로

실제로 사용할 때에는

다양한 기능을 제공하는 오픈 소스를 사용하는 것이 좋다.


데이터를 정렬하는 기능은

그림과 같이 테이블 테그의 헤드(TD, TH)를

마우스로 클릭하면

해당 컬럼(필드)의 데이터를

오름차순이나 내림차순으로 정렬한다.

이해를 돕기 위해

다음과 같이 세부 단계로 나누어서

개발하고 정리한다.


1. 버튼을 누르면 오름차순으로 정렬

2. 정렬할 데이터가 없을 때 까지 반복

3. 정렬 버튼을 누르면 오름차순로 정렬하고, 다시 버튼을 누르면 내림차순으로 정렬, 다시 버튼을 누르면 오름차순로 반복.

4. 3번 예제 단순화

5. JQuery로 변환

6. 버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면 해당 필드가 정렬

7. 필드별 정렬

8. 필드별 정렬 상태 표시

9. 라이브러리화

5단계 부터 9 단계는 나누어서 별도의 페이지에 정리하였다.

각 단계별로 간단한 구현 방법을 먼저 정리하였으니

코드를 보기 전 구현을 시도해 보고

코드를 보는 것이 실력 향상에 조금 더 도움이 될 것이다.


이러한 단계를 진행하기 전에

주어진 데이터를 동적으로 생성하는 것 부터 시작한다.


다음과 같이 주어진 2차원 배열의 데이터를 이용하여

HTML 테이블(table) 태그를 (그림참조)

자바 스크립트(JavaScript)만으로 생성한다.

var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Brad', 50, 90, 90],
            ['Lee', 70, 70, 70],
            ['Kim', 90, 60, 80]
           ];


먼저, 자바 스크립트에서 HTML 태그들을

동적으로 생성하는 방법을 알아야 한다.

동적으로 생성하는 명령어는 createElement로

함수 파라미터로 HTML 태그를 지정하면 해당 태그가 생성된다.

HTML 태그들은 계층구조로 구성되어 있기 때문에

appendChild나 insertBefore등으로 생성된 태그의 부모를 지정해야 한다.

부모를 지정하지 않으면 웹 페이지에 출력되지 않는다.

보다 상세한 설명은 w3school이나 MDN을 읽어보길 바란다.


주어진 2차원 배열의 데이터를

HTML 테이블(table) 태그로 생성하는 방법은

다음과 같다.

먼저, 테이블을 생성한 부모를 지정한다.

부모 태그는 body로 해도 되고

특정한 div(targetPn)로 해도 된다.

여기서는 HTML div 태그를 찾기 위해

ID가 targetPn인 div 태그를 getElementById를 이용하여 찾았다.


주어진 데이터는 2차원 배열이고

테이블은 행(TR)과 열(TD)로 구성되어 있다.

즉, 2차원 배열에서 각 차원이 행이 되고,

각 차원의 요소가 열이 된다.


따라서 각 차원의 개수만큼 반복해서 [라인 23]

테이블의 자식으로 행(TR)을 생성하고 [라인 24, 25]

각 차원의 요소만큼 반복해서 [라인 26]

행(TR)의 자식으로 열(TD)을 생성한다 [라인 27, 28].

생성 후 열의 값(innerHTML)으로 배열의 값을 지정한다 [라인 29].


웹브라우저의 개발자 도구(DOM 탐색기, Elements)에서

계층형으로 생성된 테이블 태그를 확인할 수 있다.


이제 본격적인 세부 구현을 진행한다.

위 실행 코드에

다음과 같이 버튼 태그를 추가하고

이 버튼을 클릭하면 데이터가 오름차순으로 정렬 되게 한다.

    <button type='button' onclick='sortTable()'>Sort</button>
    <div id='targetPn' style='width:130px'></div>

정렬하는 방법은 다음과 같다.

1. 테이블의 (행의 개수-1) 만큼 반복해서

2. 현재 행(row)과 다음 행(row+1)의 지정된 셀(td) 값을 비교

3. 현재 행의 셀이 다음 행의 셀 값보다 크면

4. 다음 행을 현재 행 앞으로 이동시킨다.



위의 문장은 각각 다음과 같이 해당 라인으로 구현된다.

1. 테이블의 (행의 개수-1) 만큼 반복해서 [라인 42, 43]

2. 현재 행(row)과 다음 행(row)의 지정된 셀(td) 값을 비교 [라인 44, 45]

3. 현재 행의 셀이 다음 행의 셀 값보다 크면 [라인 46]

4. 다음 행을 현재 행 앞으로 이동시킨다 [라인 47].


이상의 코드에서 특징적인 것 중 하나는

getElementsByTagName를 사용한 것이다 [라인 41].

getElementsByTagName는 HTML 태그로 개체를 찾는 명령어로

해당 웹 페이지에 있는 모든 태그를 찾아서 배열로 반환한다.

현재 웹 페이지에는 하나의 테이블만 있기 때문에

0 번째 테이블 태그를 사용하였다 [라인 42].

이외에도 getElementById, getElementsByName, getElementsByClassName등이 있다.


행의 개수 만큼 비교하는 반복 처리는

두번째 행(i=1) 부터

행의 개수(rows.length) 보다 1 작을 때까지 반복한다 [라인 43].

1부터 시작하는 것은

첫번째 필드(i=0)는 필드명으로 정렬하지 않는다.

행의 개수(rows.length)보다 1 적게 반복하는 것은

현재 행과 다음 행을 비교하는데

마지막 행은 다음 행이 없기 때문이다.

따라서, 전체적으로 행의 개수보다 2 적게 반복한다.


table[0].rows를 rows 변수로 지정해서 사용한 것은 [라인 42].

코드를 좀더 보기 좋고 속도(?)를 향상 시키기 위한 것이다.

하위 태그를 사용하기 위해

모든 계층 관계를 점(.)으로 나열하기 보다는

위와 같이 변수로 지정해서 사용하는 것이 좋다.


0 번째 셀(TD) 즉, 사람 이름으로 정렬한다.

제공된 데이터에서 사람 이름이 영문으로 제공되었기 때문에

대소문자 구분을 하지 않기 위해 소문자(toLowerCase)로

변환해서 비교하였다 [라인 46].


비교 결과 현재 행의 이름이 크면

다음 행을 현재 행 앞으로 이동(insertBefore) 시킨다 [라인 46].

insertBefore는 appendChild와 같이 부모를 지정하는 함수이다.

appendChild는 지정된 자식(Node)를

부모의 마지막 자식 뒤에 추가하는 것이고

insertBefore는 지정된 자식(형제) 앞에 삽입하는 함수이다.

         부모. appendChild ( 자식 )

         부모. insertBefore ( 자식, 특정 위치의 형제)

여기서는 현재 행의 부모를 찾아서 행의 이동을 처리했다 [라인 47].

현재 노드의 parentNode 속성으로

부모 노드를 찾을 수 있다.

행의 부모는 테이블 태그이기 때문에

      table[0].insertBefore(rows[i + 1], rows[i])

로 작성해도 된다.

다만, 여기서는 사용하지 않았지만 TBODY를 사용한 경우

이 코드는 오류를 발생시킨다.

다음과 같이 TBODY를 이용해서 작성해야 한다.

     table[0].tbody.insertBefore(rows[i + 1], rows[i])

이렇게 사용할 수도 있고 안 할 수도 있는 경우를 고려해서

parentNode를 이용하는 것이 좋다.


실행 후

버튼을 클릭해서 다음과 같이 정렬이 되는지 확인한다.

데이터를 다음과 같이 수정하여 실행하면

이름이 제대로 정렬되지 않은 것을 확인 할 수 있다.

var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Kim', 90, 60, 80],
            ['Lee', 70, 70, 70],
            ['Brad', 50, 90, 90]
           ];

4 번째에 있던 Brad가

1 번째가 아닌 3 번째에 있다.

이 버그를 해결하려면,

정렬 버튼을 여러 번 클릭해야 한다.


현재 행과 다음 행만 비교 하기 때문에 발생하는 문제로

여러 번 클릭하면 해결되는 것이 해법이다.

한번 클릭할 때 마다

현재 행과 다음 행을 비교해서 정렬하기 때문에

계속 버튼을 클릭하면

계속 비교를 하게 되어 제대로 정렬이 이루어 진다.

더 이상 정렬할 데이터가 없으면

클릭해도 변화가 없다(비교는 계속 이루어진다).

따라서, 제대로 정렬이 되려면

크기를 비교해서 위치를 바꾸어 주는 것을

정렬할 데이터가 없을 때 까지 반복하면 된다.

    var rows = table[0].rows;
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() > sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }

전체 코드

체크 변수(chkSort)를 사용해서

변수의 값이 true일때만 반복 하도록 하였다 (while).

For 문은 데이터 개수(length) 등 반복 범위가 명확 할 때 사용하고

while문은 그렇지 않을 때 사용한다.

while문이 시작되면

체크 변수의 값을 false로 초기 값을 지정한다.

비교문에서 정렬 데이터가 발생하면

체크 변수의 값을 true로 바꾸어

정렬 데이터가 더 있을 수 있으니

처음부터 다시 확인하도록(반복하도록) 한다.


이상의 정렬은 거품 정렬(Bubble sort) 개념을 활용한 것인데

일반적으로 거품 정렬에서는 두번의 for문을 이용한다.

while문이 아니라 이 방법으로 구현해 보길 바란다.


이번에는 정렬 버튼을 누르면 오름차순로 정렬하고,

다시 버튼을 누르면 내림차순으로 정렬,

다시 버튼을 누르면 오름차순로 반복하는 기능을 구현한다.


현재 구현된 코드는 오름차순이다.

오름차순은 큰 값과 작은 값을 비교해서 큰 값을 뒤로 보낸다.

현재 행의 셀 값이 다음 행의 셀 값보다 크면

현재 행을 다음 행 뒤로 보낸다.

다만, 코드로 구현할 때는

다음 행을 현재 행 앞으로 이동 시켰다.


내림차순은 반대로

큰 값과 작은 값을 비교해서 작은 값을 뒤로 보낸다.

현재 행의 셀 값이 다음 행의 셀 값보다 작으면

현재 행을 다음 행 뒤로 보낸다.


버튼을 눌렀을 때,

현재 정렬 상태가 오름차순이면 내림차순으로

내림차순이면 오름차순으로 정렬되도록 한다.

var sortType = 'asc';
function sortTable(){
    var table = document.getElementsByTagName('table');
   
    if (sortType === 'asc') {
        sortType = 'desc';
        sortTableDesc(table[0].rows);
    } else {
        sortType = 'asc';
        sortTableAsc(table[0].rows);
    }
}
function sortTableAsc(rows){
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() > sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }
}
function sortTableDesc(rows){
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var fCell = rows[i].cells[0];
            var sCell = rows[i + 1].cells[0];
            if (fCell.innerHTML.toLowerCase() < sCell.innerHTML.toLowerCase()) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                chkSort = true;
            }
        }   
    }
}

전체코드

하나의 버튼으로 오름차순과 내림차순을 구현하려면

전역 변수로 상태 변수(sortType)를 지정해서

변수의 값이 오름차순이면(sortType === 'asc') 내림차순(sortType = 'asc')으로

내림차순이면(sortType === ' desc' 여기에서는 else)

오름차순이(sortType = 'desc') 되도록 하면 된다.

전역 변수(함수 밖)로 선언해야 하는 이유는

함수 내에 선언되어 있으면 지정된 값이 초기화 되기 때문이다.


그리고 각각의 상태에 따라

오름 차순 함수(sortTableAsc)나

내림 차순 함수(sortTableDesc)를 호출한다.


이 두 함수를 자세히 보면

비교연산자(<>)만 다르고 모든 코드가 동일하다.

오름차순은 현재 행의 값이 다음 행의 값 보다 크면(>)

다음 행을 현재 행 앞으로 이동시키고

내림차순은 현재 행의 값이 다음 행의 값 보다 작으면(<)

다음 행을 현재 행 앞으로 이동시킨다.


이상의 코드는

중복 코드도 많고 코드 양도 너무 많다.

이번에는 조금 줄여서 작성해 본다.


비슷한 코드로 구성된 두 함수를 하나로 작성한다.

두 함수의 차이는 비교 연산자뿐이다.

var sortType = 'asc';
function sortTable(){
    sortType = (sortType === 'asc') ? 'desc':'asc';

    var table = document.getElementsByTagName('table');
    var rows = table[0].rows;
    var chkSort = true;
   
    while (chkSort){
        chkSort = false;
        for (var i = 1; i < (rows.length - 1); i++) {
            var row = rows[i];
            var fCell = row.cells[0].innerHTML.toLowerCase();
            var sCell = row.nextSibling.cells[0].innerHTML.toLowerCase();
            if ( (sortType === 'asc'  && fCell > sCell) ||
               (sortType === 'desc' && fCell < sCell) ) {
                row.parentNode.insertBefore(row.nextSibling, row);
                chkSort = true;
            }
        }   
    }
}

전체코드

두 개의 정렬 함수를 사용하던 코드를

오름차순만 구현한

이전 코드에 간단한 추가만으로 작성하였다.

먼저, 상태에 따라 변수와 함수를 호출해야 해서

IF문을 사용했지만

함수를 호출하지 않으면 하나의 라인이 되기 때문에

? 문으로 작성할 수 있다. (sortType === 'asc') ? 'desc':'asc';


상태에 따라 정렬하는 것도 코드에서 보이는 것처럼 매우 간단하다.

하나의 IF문에

오름차순이면 > 비교연산자가

내림차순이면 < 비교연산자가 적용되도록하면 된다.


이 예제는 조금 중요한 개념(?)을 설명한다.

실제 개발에서 앞서의 방식으로 작성하는 경우가 아주 많다.

이 경우 실행되기 때문에 개발이 완료 되었다고 할 수 있지만

수정 등의 유지 보수에 많은 시간을 소비할 수 있다.

여유로운 개발을 위해서 뒤의 방식으로 처리하는 습관을 들여야 한다.


마지막으로,

다음 행을 row-1로 찾았던 것을

nextSibling라는 메소드로 작성하였다.

nextSibling은 현재 노드(나)의

다음 번 형제를 찾아주는 기능을 한다.

반대 기능을 하는 previousSibling이 있으며,

이외에도 children등 자식 (노드)관련 메소드가 있다.



참고

거품 정렬에 대한 이해가 부족하다면

https://visualgo.net/en/sorting 사이트를 참고하면 된다.

다양한 정렬 알고리즘을 시각적으로 보여줘서

조금 더 쉽게 이해할 수 있게 해준다.


사이트에 접속하면

다음과 같은 화면이 실행된다.


매우 많은 기능이 제공되니 읽어보길 바라고

[X Esc]를 클릭한다.

페이지 상단에 제공되는 여러 정렬 알고리즘이 나열되어 있다.

이중 가장 앞에 있는 거품정렬(bubble sort)을 선택하고,

좌측 하단의 [Sort]를 클릭하면 나타나는

Go 메뉴를 선택하면

정렬 알고리즘이 진행되는 과정을 볼 수 있다.




앞서 작성한 테이블 데이터 정렬(table sorting) 기능을

JQuery로 변환한 후,

다음과 같은 세부 기능을 구현한다.


1. 버튼을 누르면 오름차순으로 정렬

2. 정렬할 데이터가 없을 때 까지 반복

3. 정렬 버튼을 누르면 오름차순로 정렬하고, 다시 버튼을 누르면 내림차순으로 정렬, 다시 버튼을 누르면 오름차순로 반복.

4. 3번 예제 단순화

5. JQuery로 변환

6. 버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면 해당 필드가 정렬

7. 필드별 정렬

8. 필드별 정렬 상태 표시

9. 라이브러리화


자바 스크립트로 작성된 코드를

JQuery로 바꾸기 위해

7라인과 같이 JQuery를 사용할 수 있도록 한다.

페이지 로딩이 완료된 후 발생하는 이벤트는

window.onload인데 JQuery에서는

$( document ).ready등으로 구현한다.


자바 스크립트에서는 createElement로 생성하는 것을

JQuery 기호에 생성한 HTML 태그만

파라미터로 지정하면 된다 [라인 22].

appendChild 대신에 append를 사용한다 [라인 23].

비슷한 구조로 되어 있는 것 같은데

테이블(table) 테그를 생성한 후

속성 지정을 메소드 체인(Method Chaining)으로 작성하였다.

자바 스크립트에서는

개별 속성을 하나씩 직접 지정하거나

setAttribute로 하나씩 호출해야 하는데

JQuery에서는 쭉이어서 작성할 수 있다 [라인 22].


For문 대신에 each문을 사용하여

2차원 배열 데이터의 반복 처리를 구현하였다 [라인 25, 28].


테이블의 각 셀에 값을 지정하는 것은

innerHTML 속성 대신에

JQuery의 html() 함수를 사용한다 [라인 30].


다음으로, 중요한 개념 중 하나인 선택자(selector)를 사용하였다.

앞서 작성한 예제는

행 정보에 접근하기 위해

getElementsByTagName 테이블을 찾고

table[0].rows로 테이블에 대한 행의 정보를 찾았다.


이번에는 선택자(selector)를 이용하여

$('table > tr')와 같이 간단하게 작성하였다 [라인 43].

getElementsByTagNam과 같이 테이블(table) 태그를 찾고

그 테이블 태그 바로 아래(>)에 있는 행(tr)을 반환한다.

여기서는 사용하지 않았지만

만약, tbody가 사용되었다면

이 코드에서는 반환 값이 없다.

$('table > tbody > tr')나 $('table tr')로 사용해야 한다.

자식(>) 기호는 바로 직계 자식을 의미하고

기호가 없으면 모든 자식(자식, 손주, 증손주 등)을 의미한다.

JQuery에서

HTML 태그는 별도의 표시 없이 사용했지만 (getElementsByTagName)

CSS 클래스 이름으로 찾을 때는 점(.)을 (getElementsByClassName)

ID로 찾을 때는 # 을(getElementById)

이름으로 찾을 때는 name=XXX (getElementsByName)

로 작성하면 된다.

보다 자세한 내용은 찾아보길 바라고

자바 스크립트에서는 document.querySelector로

선택자를 사용할 수 있다.


JQuery 선택자는 42 라인에 주석으로 작성 한 것과 같이

Find 메소드로도 구현할 수 있다.


이렇게 반환된 행 정보를

for문 대신 each문으로 작성하여

현재 행과 다음 행의 셀 값 비교를 처리 하였다.

현재 행은 파라미터(row)로 전달되고

다음 행은 현재 행의 다음 형제(nextSibling)로 찾는다 [라인 45, 46].

행에 대한 정보는

each문에서 row로 자바 스크립트 개체가 전달 되었기 때문에

49 라인에서 작성한 행 이동 처리를 (insertBefore)

자바 스크립트로 처리해도 되지만

사용법을 익히기 위해 JQuery로 작성하였다.

자바 스크립트 개체를 JQuery 기호 ($)로 지정하여

JQuery 개체로 변환하여 사용한다.

JQuery 개체로 사용한다는 것은

JQuery 명령어 등을 사용할 수 있다는 것을 의미한다.

앞서 정리한 선택자로 반환한 개체들도

JQuery로 지정한 것들은 JQuery 개체로 반환된다.


이상의 코드에서

자바 스크립트나 JQuery나 큰 차이는 없어 보이지만

JQuery가 좀더 단순하게 작성할 수 있는 장점이 있다.

특히, 메소드 체인은 JQuery의 장점 중 하나이다.

다만, 선택자는 강력하고 편리하지만

주의 해서 사용해야 한다.

여기에서는 테이블 태그가 하나이기 때문에 문제가 없지만

여러개가 있는데 $('table > tr')로 사용하면

문제가 생기게 된다.


이번에는

버튼 대신 각 테이블의 필드명(첫 row)을 클릭하면

해당 필드가 정렬되도록 작성한다.


첫 행을 클릭하면 해당 필드가 정렬되도록

첫 행을 생성할 때

각 필드에 클릭 이벤트를 연결한다(Binding).

해당 이벤트에서는 클릭된 필드가 반환되고

클릭된 필드의 위치(index)을 정렬할 때 참고 하면 된다.

$(function() {
    var table = $('<table>').attr('border', 1).attr('width', 200);
    $('#targetPn').append( table );
   
    $.each(data, function(inx, row ) {
        var tr = $('<tr>');
        table.append(tr);
        $.each(row, function(inx, col ) {
            var td = $('<td>');
            tr.append(td.html(col));
        });
        if (inx===0){
            tr.find('td').click(function() {
                sortTable(this.cellIndex)
            });
        }
    });
}

var sortType = 'asc';
function sortTable(cellIndex){
    sortType = (sortType === 'asc') ? 'desc':'asc';

    var chkSort = true;
    while (chkSort){
        chkSort = false;
        $('table > tr').each(function(inx, row) {
            if (inx===0 || !row.nextSibling) return;
            var fCell = row.cells[cellIndex].innerHTML.toLowerCase();
            var sCell = row.nextSibling.cells[cellIndex].innerHTML.toLowerCase();
            if ( (sortType === 'asc'  && fCell > sCell) ||
               (sortType === 'desc' && fCell < sCell) ) {
                $( row.nextSibling ).insertBefore( $(row) );
                chkSort = true;
            }
        });
    }
}

전체 코드


반복문의 색인 변수(inx) 값이 0 이면 (inx===0)

데이터의 첫 번째라는 의미로

첫 번째 행이 된다.

첫 번째 행이 생성되고 나면

해당 행의 td를 모두 찾아서(find)

마우스 클릭(click) 이벤트를 지정하였다.


이 코드는 다음과 같이

별도의 함수로 작성하고 (mouseClick),

이 함수를 연결(on, bind)해도 된다.

코드가 간략해서 앞서의 코드처럼 많이 사용한다.

function mouseClick() {
    sortTable(this.cellIndex)
};

~~ 생략 ~~

        if (inx===0){
            tr.find('td').on('click', mouseClick);
        }


자바 스크립트 개체인 this를 이용하여

현재 이벤트를 발생시킨 개체를 알 수 있다.

이벤트를 발생시킨 개체는

event.target로도 알 수 있고

this로도 알 수 있다.

현재 클릭 이벤트는 td에 지정되어 있기 때문에

this는 클릭된 td를 의미한다.


정렬에서 필요한 것은 클릭된 td의 위치이기 때문에

td의 cellIndex 속성으로 행 내에서의 위치 값을 찾아서,

정렬에서 값을 비교하는 부분에

첫 번째 필드(cellIndex=0)로 고정되어 있는 것을

cellIndex 값으로 수정하면 된다.

실행 후 각 필드를 클릭해 보면

정렬이 되는 것을 알 수 있다.

다만, 이름(Name) 필드를 오름차순으로 정렬하고

Java 필드를 클릭하면,

Java 필드의 내용이 내림차순으로 정렬된다.

다시 이름 필드를 클릭하면

이름 필드의 내용이 오름차순으로 정렬된다.

이름 필드는 이전이 정렬 상태가 오름차순이었으니

이름 필드는 내림차순으로 정렬되어야 한다.

이것은 정렬 상태를 저장하는 변수를 하나만 사용했기 때문이다.

각 필드 별로 정렬 상태를 저장하고

필드를 한번 클릭하면 오름차순,

두 번째는 내림차순이 되게 구현한다.


이것을 구현하는 방법은 정렬 상태(sortType)를

필드 개수 만큼 사용하면 된다.

    $.each(data, function(inx, row ) {
        var tr = $('<tr>');
        table.append(tr);
        $.each(row, function(inx, col ) {
            var td = $('<td>');
            tr.append(td.html(col));
        });
    });
   
    $(table).find('tr:nth-child(1)').find('td').click(function() {
        sortTable(this);
    });
});

function sortTable(cell){
    var sortType = jQuery.data( cell, 'sortType');
    sortType = (sortType === 'asc') ? 'desc':'asc';
    jQuery.data( cell, 'sortType', sortType);

    var cellIndex = cell.cellIndex;
    var chkSort = true;
    while (chkSort){
        chkSort = false;
        $('table > tr').each(function(inx, row) {
            ~~ 생략 ~~
        });
    }
}

전체 코드

정렬 상태(sortType)를 필드 개수만큼 사용하는 것은

필드 개수만큼 1차원 배열을 사용하는 것이 가장 쉽다.

이 방식은 자바 스크립트의 특징을 제대로 활용하지 못한 것으로

여기서는 필드(TD)의 속성(attribute)을 이용하였다.

HTML 태그의 속성을 이용하는 방법은 많지만

JQuery에서 제공되는 data 함수를 이용하였다.

jQuery.data()나 $.data() 로 사용하면 된다.


다만, sortTable() 함수를 호출할 때

파라미터로 클릭된 필드의 위치(cellIndex)를 지정했지만

정렬 상태를 저장하고 불러오는 기능을 구현하기 위해

클릭된 필드(this) 자체를 파라미터(cell)로 사용하였다.


앞서의 예에서는

첫 행(컬럼명)에 클릭 이벤트를 적용하기 위해

행을 생성할 때 0 번 째 행이면

모든 필드에 클릭 이벤트를 적용하였다.

이번에는 모든 행을 생성하고 난 뒤,

테이블(table) 태그의 첫 번째 행(tr:nth-child(1))을 찾아서

모든 필드에 클릭 이벤트를 적용하였다.

       $(table).find('tr:nth-child(1)').find('td').click

가급적 가독성, 속도 등을 위해 IF문을 적게 사용하는 것이 좋고

자바 스크립트의 선택자를

잘 활용하는 것이 쉬운 프로그래밍을 할 수 있는 지름길이다.


각 필드별로 정렬이 되지만

현재 어떤 필드가 정렬되었고

오름차순(▲)으로 정렬될 것인지 내림차순(▼)으로 정렬된 것인지 알 수 없다.

필드별로 정렬 상태를 표시하도록 구현한다.


현재 진행할 정렬이

오름차순인지, 내림차순인지에 따라

클릭된 필드에 기호를 넣어주면 된다.

다만, 기호를 넣기만 하면

필드에 기호가 잔뜩 쌓이기 때문에지우는 기능이 필요하다.

function sortTable(cell){
    $('table > tr:nth-child(1)').find('td').each(function(inx, td) {
        td.innerHTML = td.innerHTML.replace(/[▼▲]/g, '') ;
    });
    var sortType = jQuery.data( cell, 'sortType');
    if (sortType === 'asc') {
        sortType = 'desc';
        cell.innerHTML += '▼';
    } else{
        sortType = 'asc';
        cell.innerHTML += '▲';
    }
    jQuery.data( cell, 'sortType', sortType);
    var cellIndex = cell.cellIndex;

전체 코드

필드별로 정렬 상태를 표시하기 위해

필드를 클릭하면 정렬 상태에 따라

기호(▼▲)를 넣어 주면 된다.


다만, 클릭된 셀(td)이

JQuery 개체가 아닌

자바 스크립트 개체이기 때문에

innerHTML로 필드명 뒤에 기호를 붙여 주었다.


현재 어떤 필드가

오름차순이나 내림차순으로 정렬했는지 만 보여주기 위해

다른 필드에 남아있는 기호는 모두 지워준다. replace(/[▼▲]/g, '')

모든 필드에 있는 기호를 지우고

현재 클릭된 필드에 추가하는 방식으로 구현하고

모든 필드는

앞서 클릭 이벤트에서 처리한 선택자로 구현하였다.


정렬 상태에 따라 처리하는 내용이

한 행이 아니라서 ? 대신에 IF문으로 처리한다.


현재 코드를 다른 웹 페이지에 사용하기 위해서는

관련 코드를 복사해서

해당 페이지에서 붙여 넣기를 해야 한다.

이렇게 사용하는 것보다

라이브러리화해서 데이터만 바꾸어

호출해서 사용하는 것이 편리하다.

마지막으로 간단하게 라이브러리로 만들어 본다.


라이브러리화는 복잡하게 작성하면

자바 스크립트의 클래스를 알아야 한다.

여기서는 관련 코드를 외부 파일로 저장하고

함수를 호출하듯이 작성한다.

<script src='tableSort9.js'></script>
<script>
var data = [
            ['name', 'Java', 'Node', 'Perl'],
            ['Gu', 80, 70, 30],
            ['Kim', 90, 60, 80],
            ['Lee', 70, 70, 70],
            ['Brad', 50, 90, 90]
           ];

$(function() {
    tableSoter9('targetPn', data);
});
</script>

 tablesort9.html

function tableSoter9(target, data){
    var table = $('<table>').css({'border': '1px solid', 'width': '300px'});
    $('#'+target).append( table );
~~ 생략 ~~

tableSort9.js

데이터를 제외하고

기존에 작성된 자바 스크립트 코드만 tableSort9.js 파일로 작성한다.

load 이벤트 대신 tableSoter9()라는 함수로 바꾸어 준다.

기존의 HTML파일에 있는 load 이벤트에서 

tableSoter9.js 파일에 있는

tableSoter9() 함수를 호출하는 방식으로 구현한다.

이 함수의 파라미터로

테이블 태그를 생성할 부모 태그(targetPn)와

테이블 태그 생성시 이용한 데이터(data)를 지정한다.


HTML파일에서 tableSoter9() 함수를 호출할 수 있도록

tableSort9.js 파일을 <script> 태그로 포함시켜 준다.


어떠한 파일에서도

tableSort9.js 파일을 <script> 태그로 포함시킨 후

tableSoter9() 함수만 호출해서 사용할 수 있다.



지금까지 테이블 태그의 데이터를 정렬(table sorting)을 구현하면서

자바 스크립트의 기본 문법과

Jquery 사용법을 익힐 수 있도록 정리하였다.

정리한 내용은 모두 기초적인 것으로

잘 익혀두면 유용하게 사용할 수 있을 것이다.


실제 라이브러리로 사용하기 위해서,

다음의 남아 있는 기능을 보완하면 좋다.

실력 향상을 위해 직접 구현해 보면 좋을 것 같아서

여기에 정리하지 않았다.


남아 있는 기능

1. 현재 작성된 코드는 사용하는 웹 페이지에 하나의 테이블 태그가 사용되었다는 전제로 작성되었다.

실제 사용에서는 수많은 테이블 테그가 사용되기 때문에 문제가 발생한다.

2. 사용된 성적은 60, 70과 같은 두자리 숫자를 사용했다.

6, 7 같은 한 자리 숫자나 600, 700 같은 세 자리 숫자 등을 이용하면 그림과 같이 정렬이 제대로 이루어 지지 않는다.






'JavaScript > 기타' 카테고리의 다른 글

3. 기초 - 테이블 컬럼 이동 (Drag and Drop Table Columns)  (2) 2017.06.29
1. 기초 - 테이블 정렬 (table sorting)  (1) 2017.06.07
CD9 - Chart Design Tool for C3  (0) 2017.03.26
2. gu-upload  (4) 2016.06.18
1.guupload - 예제 설치  (0) 2016.06.18

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

CD9은 C3.JS (D3-based library) 챠트를 손쉽게 이용할 수 있게 하는 도구이다.

소스는 Github에서 받을 수 있고,

데모는 여기에서 확인할 수 있다.

C3는 D3 라이브러리를 이용하여 만든 챠트 라이브러리 이고,

D3 (Data-Driven Documents)는 웹에서 데이터를 시각적으로 쉽게 조작할 수 있도록 만든 라이브러리이다.

C3는 제법 괜찮은 챠트를 제공하지만 조금 사용하기 어려움이 있어서 쉽게 사용할 수 있는 도구를 제작하였다.

CD9은 챠트 설정, 데이터 입력, 미리 보기 부분으로 구성되어 있다.


사용자가 직접 데이터를 입력하고

원하는 상세한 차트 설정을 지정하여 즉시 확인 할 수 있다.

확인된 설정은 작성 기능을 이용하여

C3JS 챠트 사용에 필요한 자바 스크립트 코드와 CSS 설정을 생성 할 수 있다.

크롬과 firefox에서는 잘 작동되지만

IE에서는 HTML5의 숫자와 색 입력이 제대로 작동되지 않는다.

자바 스크립트(JavaScript)로 아날로그 시계를 제작하면서

HTML Canvas 사용법과

다양한 방법(2가지)으로 개발하는 것을 경험해 본다.


먼저, w3schools 사이트에 단계별로 구현된 아날로그 시계를 정리하였다.

간단한 설명도 있지만,

초보자에게는 어려움이 있어서 나름의 방법으로 정리해 보았다.


두 번째는 w3schools의 예제를 삼각 함수를 이용해서 구현해 본다.

같은 기능을 다른 방식으로 구현해 보면서 프로그래밍 이해도를 높인다.


w3schools 아날로그 시계 제작은 다음과 같이 총 5단계로 구성되어 있다.

1. 시계처럼 동그라미 그리기

2. 시계의 외곽선 그리기 (외곽선을 그라데이션으로 처리)

3. 시간(숫자) 표시

4. 시계 바늘 표시

5. 시계 작동 시키기


이 글을 읽기 전에

자바 스크립트와 CANVAS의 기초 개념은 알고 있어야 한다.

선(lineto), 도형(arc) 등을 그리는 함수들에 대해서 알고 있다는 전제로 시작한다.

따라서, 시계의 외곽선을 그리는 1, 2 단계는 정리하지 않는다.

5단계는 setInterval 함수로 1초(1000)마다 1부터 4단계까지를 다시 실행하는 것으로

별 내용이 없어 정리하지 않았다.

핵심이 되는 3단계와 4단계를 중심으로 정리하였다.

즉, 적절한 위치에 시간을 표시하고 (3단계)

현재 시간에 맞추어 시, 분, 초를 표시하는 방법을 정리하였다 (4단계).



시계의 외곽선을 그리고 나면

외곽선 안쪽에 1부터 12까지의 숫자를 배치해야 한다.


w3schools 의 숫자 출력 부분을 보면(3단계)

위치 이동을 이용하여 출력하는

다음과 같은 자바스크립트 코드를 볼 수 있다.


변수 radius 는 반지름을 의미한다.

첫 예제인 1단계 코드를 보면

주어진 Canvas 크기의 반으로 지정된 것을 볼 수 있다.

var radius = canvas.height / 2;


이 예제의 특징이 Canva의 크기에 따라

시계의 크기도 바뀌게 작성되어

시간을 표시하는

숫자의 폰트는 반지름의 15% 크기이고, (radius*0.15)

시간을 출력하는 위치는

중앙으로 부터 반지름의 85% 되는 부분이다. (radius*0.85)

위 코드의 4라인과 10라인을 보면 된다.


가장 중요한 코드가 8~11라인인데

몇가지 개념에 대해서 알아야 한다.


먼저, 12개의 숫자를 하나의 원안에 표시하기 위해서는

간단하게 생각해서 360도의 각도를 12로 나누어서 위치를 지정하면 된다.

즉, 각 숫자는 360 / 12 (30) 도씩 증가하면된다.

그런데, 자바스크립트에서는 각도를 사용하지 않고 라디안(Radian)을 사용한다.

라디안에서는 360도가 아니라 2파이(PI*2)를 사용한다.

따라서 각 숫자는 PI * 2 / 12 씩 증가하면 된다.

즉, 8라인에 작성된 코드와 같이

각 숫자가 배열될 위치는 숫자 * PI / 6의 공식이 나오게 된다 [라인 8].


두번째 개념은 Canvas의 translate함수에 대해서 이해를 해야 한다.

여기서는 다루지 않았지만,

단계 1의 시계 외곽선 그리기에서 translate가 사용되었다.

translate는 기준 좌표를 지정된 위치로 바꾸는 함수이다.

앞서, 단계 1과 2에서 3개의 원을 그렸는데

그리기 전에 Canvas의 중앙(radius, radius)으로 기준 좌표를 이동 시켜두었다.

ctx.translate(radius, radius);

이렇게 기준점을 이동시킨후

기준점을 중심으로 반지름이 radius인 각 원들을 그린 것이다.


Canvas의좌표는

다음 그림과 같이 좌측 상단이 시작 좌표로 0, 0으로 표기한다.

만약 반지름이 200인 경우

현재 기준점은 translate을 이용하여 변경한 후이기 때문에

200, 200으로 바뀌어 있다.

이 상태에서 시간을 나타내는 숫자를 표시하는데

1시는 중앙에서 우측으로 1 * PI / 6 라디안에

기준에서 - 200 * 0.85 (- radius*0.85)의 거리에 출력한다 [라인 10].


여기서 위치에 마이너스(-) 처리가 된 이유는

1시는 기준점 보다 위에 있기 때문이다.

기준점보다 위에 있다는 것은

기준점은 Y가 0 이니 마이너스가 되어야 찾아 갈 수 있다.

다음 그림에서

첫 번째 그림이 Canvas의 2차원 좌표이다.

Canvas 좌표는 좌측 상단이 0, 0으로 기준점이 된다 (첫번째 그림).

이 기준점은 translate 함수를 이용하여

특정한 좌표로 이동시킬 수 있다 (두번째 그림).


기준점은 X와 Y가 만나는 곳이 되고

기준점은 항상 0, 0 이 된다.

따라서, Y축이 기준점보다 위로 가면 음수(-) 값을

Y축이 기준점보다 아래로 가면 양수(+) 값을 가지게 된다.


세번째 그림과 같이 1시 방향에 숫자 1을 찍기 위해서

좌표 축을 30 ( 1 * PI / 6)도 기울이고,

(30도는 30/180 즉 1/6이고, 라디안으로 pi /6을 의미)

Y축 값을 음수(- radius*0.85)로 지정해야 된다.

그림을 보면 X축의 값은 변경하지 않아도 된다.

즉, 0.

1시 방향 (30도, pi /6)에 숫자 1을 찍기 위해서

축을 30도 회전시키기 위해 roate함수를 사용하고 [라인 9]

숫자가 출력될 부분으로 이동(translate) 한다 [라인 10].

다음 그림과 같이 기준점이 이동하게 된다.

이 상태에서 숫자를 출력하면

그림과 숫자가 같이 기울어진 상태에서 출력된다.

따라서, 다음 그림과 같이

앞서 지정한 각도를 음수화(-ang)해서 직각이 되게 한다.[라인 11]



이렇게 회전(+), 이동(-), 역회전(-)을 하여

각 숫자별 위치를 잡아서 출력을 하고

회전(-), 이동(+), 역회전(+)을 하여

화면 중앙으로 돌아와서

다음 숫자를 출력할 준비를 한다.

이렇게 12번을 반복하면 된다.


다시 정리하면

  1. 시간에 따른 각도(라디안) 만큼 회전
  2. 출력하기 위한 위치로 이동, Y축만 변경(마이너스)
  3. 시간을 직각으로 출력하기 위해 회전한 각도를 반대로 회전(마이너스)
  4. 숫자 출력
  5. 시간을 직각으로 출력하기 위해 반대로 회전한 각도를 정상으로 회전: 3 번 반대
  6. 변경한 Y축 값을 양수로 지정하여 중앙 기준점으로 되돌아옴: 2 번 반대
  7. 회전한 시간 방향을 12시 방향으로 재 회전(마이너스): 1 번 반대


1 번 부터 3 번을 진행하며

시간이 출력될 위치에 숫자를 출력하고

5 번 부터 7 번까지 진행하여

시계 중앙에 기준점을 다시 배치한다.

그래야 다음 시간(숫자)을 시계 중앙에서 출발해서 작성할 수 있기 때문이다.





+ Recent posts