Generator, Co, Async 를 이용한 비동기 순차 실행시키기
D3.js 를 사용하여 데이터 시각화하기 #1 Line Charts
2018-10-29
Explanation
요즘은 다양한 차트 라이브러리들이 정말 많은 기능들을 제공해줘서 편리하게 사용할 수 있는데요, 그런데 아주 세세하게 커스텀을 하다보면 아무래도 아쉬운 부분을 찾게 되는거 같아요. 그래서 알아보던 중.. 깃허브 레파지토리 스타 랭킹 7등에 빛나는 D3.js에 대해 알아보려 합니다.
인터넷을 통해 몇몇 예제들을 검색해봤는데, 대부분 3 버전의 예제들이 많았습니다. 지금 5 버전까지 릴리즈 되어 있는데 말이죠.. 처음에는 5버전의 API를 차례차례 볼까 생각했는데… 생각보다 코드의 볼륨이 엄청 커서, 순서를 바꿔서 간단한 예제 코드들을 보면서 API에 대해 알아보려 합니다.
참고 : D3 API Reference
오늘 처음 해본 거라 글에는 수많은 추측과 오류를 담고 있습니다.
예제 코드 출처
https://beta.observablehq.com/@mbostock/d3-line-chart
아래의 코드는 위 예제 코드를 아주 조금 수정한 내용입니다.
1 2 3 4 5 6 7 8 9 10 |
<!doctype html> <html lang="ko"> <head> <title>D3 line chart example</title> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <script src="line-chart.js"></script> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// line-chart.js const width = 500; const height = 500; const margin = {top: 40, right: 40, bottom: 40, left: 40}; const data = [ {date: new Date('2018-01-01'), value: 10}, {date: new Date('2018-01-02'), value: 20}, {date: new Date('2018-01-03'), value: 30}, {date: new Date('2018-01-04'), value: 25}, {date: new Date('2018-01-05'), value: 35}, {date: new Date('2018-01-06'), value: 45}, {date: new Date('2018-01-07'), value: 60}, {date: new Date('2018-01-08'), value: 50} ]; const x = d3.scaleTime() .domain(d3.extent(data, d => d.date)) .range([margin.left, width - margin.right]); const y = d3.scaleLinear() .domain([0, d3.max(data, d => d.value)]).nice() .range([height - margin.bottom, margin.top]); const xAxis = g => g .attr("transform", `translate(0,${height - margin.bottom})`) .call(d3.axisBottom(x).ticks(width / 90).tickSizeOuter(0)); const yAxis = g => g .attr("transform", `translate(${margin.left},0)`) .call(d3.axisLeft(y)) .call(g => g.select(".domain").remove()) .call(g => { return g.select(".tick:last-of-type text").clone() .attr("x", 3) .attr("text-anchor", "start") .attr("font-weight", "bold") .attr("font-size", '20px') .text('Y축') }); const line = d3.line() .defined(d => !isNaN(d.value)) .x(d => x(d.date)) .y(d => y(d.value)); const svg = d3.select('body').append('svg').style('width', width).style('height', height); svg.append("path") .datum(data) .attr("fill", "none") .attr("stroke", "steelblue") .attr("stroke-width", 1) .attr("stroke-linejoin", "round") .attr("stroke-linecap", "round") .attr("d", line); svg.append('g').call(xAxis); svg.append('g').call(yAxis); svg.node(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const width = 500; const height = 500; const margin = {top: 40, right: 40, bottom: 40, left: 40}; const data = [ {date: new Date('2018-01-01'), value: 10}, {date: new Date('2018-01-02'), value: 20}, {date: new Date('2018-01-03'), value: 30}, {date: new Date('2018-01-04'), value: 25}, {date: new Date('2018-01-05'), value: 35}, {date: new Date('2018-01-06'), value: 45}, {date: new Date('2018-01-07'), value: 60}, {date: new Date('2018-01-08'), value: 50} ]; |
이 부분은 그려질 차트의 너비와 높이, 그리고 위아래 좌우의 여백값 그리고 그래프를 그릴 데이터 값입니다.
이번 예제에서는 여기의 date는 X축으로 사용되고 value는 Y축으로 사용될 거에요.
약간의 특징적으로 d3.js는 몇개의 비표준 브라우저를 위한 메서드를 제외하고는 전역에 ‘d3’만 사용하여, 모든 기능이 네임스페이스 ‘d3’에 물려있다고 합니다.
이제부터 머리가 아프기 시작합니다..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// x 축으로 사용될 값을 설정합니다. const x = d3.scaleTime() // .scaleTime()는 x축시 시간을 기준으로 설정할 것이라고 설정? 선언? 합니다. // 내부 소스를 보지 않아서 확실하진 않지만 X축으로 사용할 값을 직접 원시 값을 넣어주지 않아도 위 메서드를 통해서 알아서 해주는 것 같습니다. // 예를 들면 data = [{ date : '2018-01-01', ...} 이렇게 직접 원시 데이터를 넣어주지 않고 new Date('2018-01-08')를 넣으면 알아서 날짜 형식으로 해주는 듯 합니다. .domain(d3.extent(data, d => d.date)) // .extent()는 위 코드로 이야기를 하면 첫번째 인자값의 데이타의 date 속성의 값중에 가장 작은값과 가장 큰값을 배열로 응답해줍니다. // ex: [new Date('2018-01-01'), new Date('2018-01-08')] .range([margin.left, width - margin.right]); // .range()는 위치값으로 최소값의 위치와 최대값의 위치를 배열로 받아 위 .domain의 값과 매칭하는 듯 합니다. const y = d3.scaleLinear() // .scaleLinear()는 위 .scaleTime() 과 비슷한 역할을 하지만 얜 선의 범위로 설정? 선언? 해준 것 입니다. .domain([0, d3.max(data, d => d.value)]).nice() // 선형태의 값으로 최소값은 0 이고 .max()는 .extent()과 비슷하게 동작해서 가장 큰 값을 응답해줍니다. // .nice()는 시작값과 끝 값을 반올림 값으로 해줍니다. // ex: [0.2123123123, 0.991234123123] > [0.2, 1] .range([height - margin.bottom, margin.top]); |
이어서…
단어 그대로 axis는 중신선을 말하고 xAxis는 X축의 중심선, yAxis는 Y축의 중심선을 의미합니다. 그리고 ticks은 X축, Y축 중심선에 구간을 나누는.. 그것…을 말합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 여기에서 인자 변수의 명칭으로 g를 사용하는게 SVG의 g 엘리먼트를 표현한 것으로 보입니다. // SVG에서 g 엘리먼트는 그룹을 의미합니다. const xAxis = g => g .attr("transform", `translate(0,${height - margin.bottom})`) // X축의 위치를 정합니다. translate(x, y) .call(d3.axisBottom(x).ticks(width / 90).tickSizeOuter(0)); // .axisBottom()는 중심선을 기준으로 아래쪽에 ticks를 위치시킵니다. // .axisLeft()는 중심선 기준 왼쪽에 위치 // .ticks()는 역시 소스코드를 보진 않았지만 값이 작아질수록 ticks의 갯수가 줄어듭니다. // .tickSizeOuter()는 tick의 바깥 선? 의 크기를 조절합니다. //.call(funcName[, arg[, arg2[, ...]]]) // 첫번재 파라미터로 설정한 이름의 함수를 실행하고, 선택적으로 두번째 이후로 그 밖의 파라미터도 함께 넘길 수 있습니다. // 첫번째 파라미터로 설정한 이름의 함수에서는 첫번째 인자값으로 .call 메서드를 실행한 객체를 this로 가리키며 // 선택적으로 두번째 인자값 이후로, 두번째 파라미터 값들을 받을 수 있습니다. const yAxis = g => g .attr("transform", `translate(${margin.left},0)`) .call(d3.axisLeft(y)) .call(g => g.select(".domain").remove()) // .select()는 document.querySelector()와 비슷하게 해당 선택자를 선택할 수 있습니다. 하나만 선택됩니다. // 복수 선택을 위해서는 .selectAll()을 사용할 수 있습니다. 이는 document.querySelectorAll()와 비슷합니다. // 여기에서 g(svg g element)중 'class="domain"' 요소를 삭제하였습니다. // 예제를 확인해보면 Y축의 중심선이 사라진걸 확인할 수 있습니다. .call(g => { console.log(g.select(".tick:last-of-type text").attr('x')); return g.select(".tick:last-of-type text").clone() // 대충 이름만 보아도 알 수 있듯이 'tick' 이라는 클레스 중 마지막에 위치한 엘리먼트의 자식 text 엘리먼트를 복제하였습니다. .attr("x", 3) // .attr() 자바스크립트의 .setAttribute()와 비슷하게 엘리먼트의 속성과 값을 셋합니다. // 두번째 파라미터 값이 없다면 .getAttribute()와 비슷하데 엘리먼트의 해당 속성의 값을 불러옵니다. .attr("text-anchor", "start") .attr("font-weight", "bold") .attr("font-size", '20px') .text('Y축') // .text() 위에서 복제한 엘리먼트에 text 값을 줍니다. .innerText()와 비슷합니다. }); |
이어서…
거의 다 왔습니다…
1 2 3 4 5 6 7 8 9 10 11 |
const line = d3.line() // .line() 데이터의 값을 선으로 선으로 표현합니다. .defined(d => !isNaN(d.value)) // defined() value의 값이 Number가 아니라면 그래프의 선이 끊겨서 출력됩니다. .x(d => x(d.date)) .y(d => y(d.value)); // 여기서 사용되는 인자명을 d로 사용하는 건 아마도 SVG의 path 엘리먼트의 속성 d를 의미하는 것 같습니다. // d 속성은 패스의 모양을 정의합니다. // * 2018-10-30 수정 // 다른 그래프들의 예제를 보니 d속성의 값이 아니여도 인자명으로 d를 사용 하는 것으로 보아 // 그냥 data를 줄여 사용하는 것 같습니다. |
마지막…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const svg = d3.select('body').append('svg').style('width', width).style('height', height); // 참고한 예제에서는 변수 svg를 // const svg = d3.select(DOM.svg(width, height)); // 이렇게 선언했는데, 어떻게 저렇게 쓰는건지 아직 모르겠어서 일단, svg를 append하고 직접 width, height 값을 주었습니다. svg.append('g').call(xAxis); // svg에 g 엘리먼트를 추가하고 아까 만든 X축의 설정을 call합니다. svg.append('g').call(yAxis); // 동일하게 svg에 g 엘리먼트를 추가하고 아까 만든 Y축의 설정을 call합니다. svg.append("path") // 그래프의 선이 될 path 엘리먼트를 추가합니다. .datum(data) // .datum() 아직 이 메서드를 잘 모르겠습니다. 데이터를 join하지 않고 가져오거나 설정한다고 합니다;; .attr("fill", "none") .attr("stroke", "steelblue") .attr("stroke-width", 1) .attr("stroke-linejoin", "round") .attr("stroke-linecap", "round") // 기본적인 svg의 스타일 속성들을 줍니다. .attr("d", line); svg.node(); // 실행? |
아주 많은 잘못된 설명이 포함되어 있을 수 있습니다. 잘못된 부분은 댓글로 알려주시면 감사하겠습니다 :)
이후에 조금 더 알게되면 계속 수정하도록 하겠습니다.