Learn to Create D3.js Data Visualizations by Example
์๋ณธ : https://www.sitepoint.com/d3-js-data-visualizationsD3 ๊ณต๋ถํ๊ธฐ ์ข์ ์๋ฃ ์ธ๊ฑฐ ๊ฐ์ ๊ฐ์ธ์ ์ผ๋ก ์ธ ์๋์ผ๋ก ๋ฒ์ญํ์์ต๋๋ค. ๋งํฌ ๋๋ ๋ค๋ฅธ๊ณณ์ ์ฎ๊ธฐ์ง ๋ง์ ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.^^
๋๋ HTML์ humble bar chart๋ก ๋ง๋๋ ๊ฒ์ document์ D3 transforms data๋ฅผ ์ดํดํ๋ ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ์ด๋ฉฐ William Playfairs charts๋ณด๋ค ๋ ์ข์ ๊ฒ์ ์ฝ์ํ๋ค. ์ฌ๊ธฐ๋ฅผ ๋ณด์.
d3.select('#chart') .selectAll("div") .data([4, 8, 15, 16, 23, 42]) .enter() .append("div") .style("height", (d)=> d + "px")
selectAll ํจ์๋ D3 โselectionโ์ return ํ๋ค : elements์ array์ ์ฐ๋ฆฌ๋ enter์ append๋ ๊ฐ ๋ฐ์ดํฐ point์ํด div ๋ง๋ค์๋ค.
์ด code maps์ [4, 8, 15, 16, 23, 42]์ ๋ค์ด์ค๊ณ , HTML๋ก ๋์จ๋ค.
<div id="chart"> <div style="height: 4px;"></div> <div style="height: 8px;"></div> <div style="height: 15px;"></div> <div style="height: 16px;"></div> <div style="height: 23px;"></div> <div style="height: 42px;"></div> </div>
๋ณ๊ฒฝ๋์ง ์์ style properties์ ๋ชจ๋ ๊ฒ์ CSS์์ ํ ์ ์๋ค.
#chart div { display: inline-block; background: #4285F4; width: 20px; margin-right: 3px; }
Githubโs Contribution Chart
์ฝ๋ ๋ช๊ฐ๋ฅผ ์ถ๊ฐํ๋ฉด ์ฐ๋ฆฌ๋ Github์ ๋น์ทํ contribution chart๋ฅผ bar chart๋ก ๋ง๋ค ์ ์๋ค.
const colorMap = d3.interpolateRgb( d3.rgb('#d6e685'), d3.rgb('#1e6823') ) d3.select('#chart') .selectAll("div") .data([.2, .4, 0, 0, .13, .92]) .enter() .append("div") .style("background-color", (d)=> { return d == 0 ? '#eee' : colorMap(d) })
colorMap ํจ์๋ 0๊ณผ 1 ์ฌ์ด์ ๊ฐ์ ์
๋ ฅํ๊ณ , ์ ๊ณตํ๋ ๋๊ฐ์ง ์ฌ์ด์ ์ปฌ๋ฌ ๊ทธ๋ผ๋ฐ์ด์
์ค ํ๋๋ฅผ returnํ๋ค. Interpolation์ programming๊ณผ animation์ ํค ๋๊ตฌ ์ด๋ค. ์ฐ๋ฆฌ๋ ๋์ค์ ๋ ๋ง์ ์์ ์์ ๋ณผ๊ฒ์ด๋ค.
D3์ ํ์ ๋๋ถ๋ถ์ SVG๋กค ์์
ํ๋ ๊ฒ์์ ์ง์ฌ์ผ๋ก ๋์จ๋ค. circles, polygons, paths text์ ๊ฐ์ 2D graphics๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์ํ ํ๊ทธ๋ฅผ ํฌํจํ๋ ๊ฒ
<svg width="200" height="200"> <circle fill="#3E5693" cx="50" cy="120" r="20" /> <text x="100" y="100">Hello SVG!</text> <path d="M100,10L150,70L50,70Z" fill="#BEDBC3" stroke="#539E91" stroke-width="3"> </svg>
circle๋ ์ขํ 50,120์ 20์ ๋ฐ์ง๋ฆ์ด๋ค.
text๋ ์ขํ 100,100์ โHello SVGโ์ด๋ค.
triangle๋ 3px์ ์ ์ ๋ค์๊ณผ ๊ฐ์ instruction attribute์ด๋ค.
150,70๊น์ง ์ ์ ๊ธ๊ณ
๋ค์ 50,70๊น์ง ์ ์ ๊ธ๊ณ
<path>๋ SVG์ ๋๋ถ๋ถ ํ์ํํ ์๋ฆฌ๋จผํธ์ด๋ค.
์ด์ ์ ์์ ์ ๋ฐ์ดํฐ ์ค์ ์ ๊ฐ๋จํ ์ซ์์ ๋ฐฐ์ด์ ๊ฐ์ง๊ณ ํ๋ค. D3๋ ๋ํ ๋ ๋ณต์กํ ํ์
์ผ๋ก ์์
์ ํ ์ ์๋ค.
const data = [{ label: "7am", sales: 20 },{ label: "8am", sales: 12 }, { label: "9am", sales: 8 }, { label: "10am", sales: 27 }]
๊ฐ ๋ฐ์ดํฐ์ ํฌ์ธํธ๋ #chart์ (group) element๋ฅผ ์ถ๊ฐํ๊ณ , ๋ฅผ ์ถ๊ฐํ๊ณ , ์ฐ๋ฆฌ์ ๊ฐ์ฒด์์ ์์ฑ๋ค์ <text> elements์ ์ถ๊ฐํ ๊ฒ์ด๋ค.
const g = d3.select('#chart') .selectAll("g") .data(data) .enter() .append('g') g.append("circle") .attr('cy', 40) .attr('cx', (d, i)=> (i+1) * 50) .attr('r', (d)=> d.sales) g.append("text") .attr('y', 90) .attr('x', (d, i)=> (i+1) * 50) .text((d)=> d.label)
๋ณ์ g๋ <g> node์ ๋ฐฐ์ด์ ํฌํจํ d3โselectionโ์ ์ก๊ณ , selection์์ ๊ฐ ์์ดํ
์ ์๋ก์ด element์ ์ถ๊ฐํ๋ append()์ ๊ฐ์ ํ๋์ด๋ค.
SVG์์ line chart๋ฅผ ๊ทธ๋ฆฌ๋ ๊ฒ์ ๊ต์ฅํ ๊ฐ๋จํ๋ค. ์ฐ๋ฆฌ๋ ์๋์ ๊ฐ์ด ๋ฐ์ดํฐ๋ฅผ transform์ ์ํ๋ค.
const data = [ { x: 0, y: 30 }, { x: 50, y: 20 }, { x: 100, y: 40 }, { x: 150, y: 80 }, { x: 200, y: 95 } ]
<svg id="chart" height="100" width="200"> <path stroke-width="2" d="M0,70L50,80L100,60L150,20L200,5"> </svg>
y์ ๊ฐ์ SVG๋ top์ด ๊ฐ์ด 0์ด๊ธฐ ๋๋ฌธ์ ์ํ๋ ๊ฐ์ผ๋ก y๋ฅผ ์ค์ ํ๋ ค๋ฉด 100์์ ์ํ๋ ์ซ์๋ฅผ ๋นผ๋ฉด ์ํ๋ ๊ณณ์ ํ์๊ฐ ๋๋ค.
๋จ์ํ path elements์ด๋ค. ์ฐ๋ฆฌ๋ ์ด์ ๊ฐ์ ์ฝ๋๋ฅผ ์ฐ๋ฆฌ ์์ ์ด ๋ง๋ค ์ ์๋ค.
const path = "M" + data.map((d)=> { return d.x + ',' + (100 - d.y); }).join('L'); const line = `<path stroke-width="2" d="${ path }"/>`; document.querySelector('#chart').innerHTML = line;
D3๋ ์ด๊ฒ์ ๋จ์ํ๊ฒ ๋ง๋๋ path generating ํจ์๊ฐ ์๋ค. ์๋๋ฅผ ๋ณด์
const line = d3.svg.line() .x((d)=> d.x) .y((d)=> 100 - d.y) .interpolate("linear") d3.select('#chart') .append("path") .attr('stroke-width', 2) .attr('d', line(data))
Scales๋ domain์ ๋ฃ๊ณ range๊ฐ ๋์ค๋ ํจ์์ด๋ค.
์์ ์์ ์ฐ๋ฆฌ๊ฐ ์ง๊ธ๊น์ง ๋ณธ๊ฒ์ ๊ฒฝ๊ณ๊ฐ ๊ณ ์ ์ ์ธ๊ฒ์ด๋ค. ํ์ง๋ง ๊ทธ๋ํ์ ๊ฒฝ๊ณ๊ฐ ๊ณ ์ ์ ์ด์ง ์๊ณ ๋ณ๊ฒฝ์ด ๋๋ค๋ฉด, ์ฐจํธ๋ ๊ฒฝ๊ณ์ ๋ง๊ฒ ํฌ์ง์
์ด ๋ณ๊ฒฝ ๋์ด์ผ ํ๋ค.
๋ค์์ ๋ฐ์ดํฐ๋ฅผ 500x200์์ line chart๋ก ๋ ๋๋ง ํด๋ณธ๋ค๋ฉด
const data = [ { x: 0, y: 30 }, { x: 25, y: 15 }, { x: 50, y: 20 } ]
d3.max๋ฅผ ์ด์ฉํ์ฌ ์ต๋๊ฐ์ ์ฐพ๋ ๊ฒ์ด๋ค. ๋ํ domain์ ์ด๋ฌํ ์ต๋๊ฐ์ ์ฌ์ฉํ์ฌ ์ฐจํธ ๊ฒฝ๊ณ๊ฐ ๋ณ๊ฒฝ ๋์์๋, ๊ฑฐ๊ธฐ์ ๋ง๊ฒ ์ฐจํธ๋ฅผ ๋ณ๊ฒฝํ๋ค.
const width = 500; const height = 200; const xMax = d3.max(data, (d)=> d.x) const yMax = d3.max(data, (d)=> d.y) const xScale = d3.scale.linear() .domain([0, xMax]) // input domain .range([0, width]) // output range const yScale = d3.scale.linear() .domain([0, yMax]) // input domain .range([height, 0]) // output range
์๋๋ ์ค์ ๊ฐ๊ณผ ๊ฒฝ๊ณ ์์์์ ๊ฐ์ ๋ณด์ฌ์ค๋ค.
xScale(0) -> 0 xScale(10) -> 100 xScale(50) -> 500
xScale(-10) -> -100 xScale(60) -> 600
์ด๋ฌํ ๊ฒ์ ์ฐ๋ฆฌ๋ line generating function์ ์์ scales๋ฅผ ์ด์ฉํ ์ ์๋ค.
์๋๋ ๊ฒฝ๊ฒ๋ฉด ์์ padding์ ์ฃผ๋ ๋ฐฉ๋ฒ์ด๋ค.
const padding = 20; const xScale = d3.scale.linear() .domain([0, xMax]) .range([padding, width - padding]) const yScale = d3.scale.linear() .domain([0, yMax]) .range([height - padding, padding])
์ง๊ธ ์ฐ๋ฆฌ๋ ๋์ ์ธ ๋ฐ์ดํฐ ์
์ผ๋ก ๋ ๋๋ง ํ ์ ์๊ณ , ์ฐ๋ฆฌ์ line chart์ 500px / 200px์ ๊ฒฝ๊ณ์ ์์ ๋ชจ๋ 20px padding์ ๋ฃ์ ๊ฒ์ด๋ค.
Linear scales๋ ๋๋ถ๋ถ์ ๊ณตํ ์ ํ์
์ด๋ค. ํ์ง๋ง exponential scalesd์ POW์ names ๋๋ categories์ ๊ฐ์ representing non-numeric data ordinal scales ๊ฐ์ ๋ค๋ฅธ ๊ฒ์ด ์๋ค. ์ถ๊ฐ์ ์ผ๋ก Quantitative Scales์ Ordinal Scales ๊ทธ๋ฆฌ๊ณ date ranges mapping์ ์ํ Time Scales๋ ์๋ค.
์๋ฅผ ๋ค์ด ์ฐ๋ฆฌ๋ 0๋ถํฐ 500๊น์ง ์ฌ์ด์ ์ซ์์์ ๋์ lifespan maps์ scale๋ฅผ ๋ง๋ค ์ ์๋ค.
const life = d3.time.scale() .domain([new Date(1986, 1, 18), new Date()]) .range([0, 500]) // At which point between 0 and 500 was my 18th birthday? life(new Date(2004, 1, 18))
Animated Flight Visualization
์ฐ๋ฆฌ๋ ์ํ๊ณ ์ข์์ง๋ง ์ง๊ธ๊น์ง ์ฐ๋ฆฌ๋ ๊ณ ์ ์ ์ธ ์๋๊ฐ์ด ์๋ ๊ทธ๋ํฝ๋ง ํ๋ค. visualization ํ ์๋๋ฉ์ด์
์ ๋ง๋์ด ๋ณด์ Australia์์ Melbourne์ Sydney๋ฅผ ํ๋ฐํ๊ฒ ๋ ๋ผ ๋ค๋ ๊ฒ์ ๋ณด์ฌ์ฃผ์
graphic์ ์ด ํ์
์์ SVG document๋ text, lines ๊ทธ๋ฆฌ๊ณ circles๋ก ๋ง๋ค์ด์ ธ ์๋ค.
<svg id="chart" width="600" height="500"> <text class="time" x="300" y="50" text-anchor="middle">6:00</text> <text class="origin-text" x="90" y="75" text-anchor="end">MEL</text> <text class="dest-text" x="510" y="75" text-anchor="start">SYD</text> <circle class="origin-dot" r="5" cx="100" cy="75" /> <circle class="dest-dot" r="5" cx="500" cy="75" /> <line class="origin-dest-line" x1="110" y1="75" x2="490" y2="75" /> <!-- for each flight in the current time --> <g class="flight"> <text class="flight-id" x="160" y="100">JQ 500</text> <line class="flight-line" x1="100" y1="100" x2="150" y2="100" /> <circle class="flight-dot" cx="150" cy="100" r="5" /> </g> </svg>
๋์ ์ธ ๋ถ๋ถ์ ํญ๊ณต๊ธฐ ๊ทธ๋ฃน์์ ์๊ฐ๊ณผ element์ด๊ณ ๋ฐ์ดํฐ๋ ์ด์ ๊ฐ์ด ๋ณผ์ ์๋ค.
let data = [ { departs: '06:00 am', arrives: '07:25 am', id: 'Jetstar 500' }, { departs: '06:00 am', arrives: '07:25 am', id: 'Qantas 400' }, { departs: '06:00 am', arrives: '07:25 am', id: 'Virgin 803' } ]
๋์ ์ธ ์๊ฐ์ ์ํ x position์ ์ป๊ณ ์ฐ๋ฆฌ๋ ์ฐ๋ฆฌ์ ์ฐจํธ์์ x position์ ์ถ๋ฐ์๊ฐ๊ณผ ๋์ฐฉ์๊ฐ ๊ฐ ๋นํ๊ธฐ๋ฅผ ์ํ time scale ๋ง๋๋ ๊ฒ์ด ํ์ํ๋ค. ์ฐ๋ฆฌ๋ Date ๊ฐ์ฒด์ scales๋ฅผ ์ถ๊ฐํ๊ธธ ์์ํ๋ฉด์ ์ฐ๋ฆฌ์ ๋ฐ์ดํฐ๋ฅผ ํตํด ๋ฐ๋ณตํ ์ ์๋ค. Moment.js๋ date parsing ๊ณผ manipulatio๋ฅผ ์ฌ๊ธฐ๋ง์ด ๋์ธ์ ์๋ค.
data.forEach((d)=> { d.departureDate = moment(d.departs, "hh-mm a").toDate(); d.arrivalDate = moment(d.arrives, "hh-mm a").toDate(); d.xScale = d3.time.scale() .domain([departureDate, arrivalDate]) .range([100, 500]) });
์ฐ๋ฆฌ๋ ๊ฐ ํญ๊ณต๊ธฐ๋ฅผ ์กฐ์ ํ๊ธฐ ์ํด x๋ฅผ ์ป๊ธฐ ์ํ xScale()๋ฅผ ์ฐ๋ฆฌ์ ๋ณํ๋ Date๋ฅผ ํต๊ณผ ํ ์ ์๋ค.
์ถ๋ฐ๊ณผ ๋์ฐฉ ์๊ฐ์ 5๋ถ๋์ ๋๊ณ ๊ทธ๋์ ์ฐ๋ฆฌ๋ ์ฒซ๋ฒ์งธ ์ถ๋ฐ๊ณผ ๋ง์ง๋ง ๋์ฐฉ์์ 5m์ฆ๊ฐํ๋ ํตํด ๋จ๊ณ๋ฅผ ํ ์ ์๋ค.
let now = moment(data[0].departs, "hh:mm a"); const end = moment(data[data.length - 1].arrives, "hh:mm a"); const loop = function() { const time = now.toDate(); // Filter data set to active flights in the current time const currentData = data.filter((d)=> { return d.departureDate <= time && time <= d.arrivalDate }); render(currentData, time); if (now <= end) { // Increment 5m and call loop again in 500ms now = now.add(5, 'minutes'); setTimeout(loop, 500); } }
D3๋ ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ ์ผ๋ ์๋ฆฌ๋จผํธ์ transformation๊ณผ transitions๊ฐ ๋ช
์๋์ด ์๋ค.
New data points come in (Enter)
Existing data points change (Update)
Existing data points are removed (Exit)
const render = function(data, time) { // render the time d3.select('.time') .text(moment(time).format("hh:mm a")) // Make a d3 selection and apply our data set const flight = d3.select('#chart') .selectAll('g.flight') .data(data, (d)=> d.id) // Enter new nodes for any data point with an id not in the DOM const newFlight = flight.enter() .append("g") .attr('class', 'flight') const xPoint = (d)=> d.xScale(time); const yPoint = (d, i)=> 100 + i * 25; newFlight.append("circle") .attr('class',"flight-dot") .attr('cx', xPoint) .attr('cy', yPoint) .attr('r', "5") // Update existing nodes in selection with id's that are in the data flight.select('.flight-dot') .attr('cx', xPoint) .attr('cy', yPoint) // Exit old nodes in selection with id's that are not in the data const oldFlight = flight.exit() .remove() }
์ด ์ฝ๋๋ 5๋ถ๊ฐ 500ms frame์ renderํ๋ค.
๋ชจ๋ ๋นํ๊ธฐ ์์์ ์ ๋นํ๊ธฐ ๊ทธ๋ฃน์ ๋ง๋ ๋ค.
ํ์ฌ ๋นํ๊ธฐ์ x/y ์์น๋ฅผ ์
๋ฐ์ดํธ ํ๋ค.
๊ทธ๋ค์ด ๋์ฐฉํ๋ฉด ๋นํ๊ธฐ ๊ทธ๋ฃน์ ์ญ์ ํ๋ค.
์ด ์์
์ ๊ทธ๋ฌ๋ ๋ฌด์์ด ์ฐ๋ฆฌ๋ ์ ๋ง ์ํ๋์ง ๋ถ๋๋ฌ์ด transition์ ํ๋ ์๋ค์ ๊ฐ ์ฌ์ด๋ฅผ. ์ฐ๋ฆฌ๋ D3 selection์ผ๋ก transition์ ๋ง๋๋ ๊ฒ์ ๋ฌ์ฑํ ์ ์๊ณ ์์ฑ๊ณผ ์คํ์ผ ์์ฑ์ ์ค์ ํ๊ธฐ์ ์ easing function์ duration์ ์ ๊ณตํ๋ค.
์๋, ๋นํ๊ธฐ ๊ทธ๋ฃธ๋ค์ ํฌ๋ช
๋๋ฅผ ํ์ด๋ ํ๋ ๊ฒ์ด๋ค.
const newFlight = flight.enter() .append("g") .attr('class', 'flight') .attr('opacity', 0) newFlight.transition() .duration(500) .attr('opacity', 1)
๋นํ๊ธฐ๊ฐ ๋๊ฐ๋ fade out ํ์
flight.exit() .transition() .duration(500) .attr('opacity', 0) .remove()
x ์ y ํฌ์ธํธ ์ฌ์ด์ ๋ถ๋๋ฌ์ด transition์ ์ถ๊ฐ
flight.select('.flight-dot') .transition() .duration(500) .ease('linear') .attr('cx', xPoint) .attr('cy', yPoint)
์ฐ๋ฆฌ๋ 5๋ถ ์ฆ๊ฐ ์ฌ์ด์ ๋ํ transition์ ํ ์ ์๊ณ , tween ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋งค 5๋ถ ๋ณด๋ค ๋งค๋ถ์ display ํ ์ ์๋ค.
const inFiveMinutes = moment(time).add(5, 'minutes').toDate(); const i = d3.interpolate(time, inFiveMinutes); d3.select('.time') .transition() .duration(500) .ease('linear') .tween("text", ()=> { return function(t) { this.textContent = moment(i(t)).format("hh:mm a"); }; });
t๋ 0๊ณผ 1 ์ฌ์ด์ transition์ด ์งํ์ค์ธ ๊ฐ์ด๋ค.
์ด ์ํฐํด๋ณด๋ค ๋ ๋ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋ค. ๋ช๊ฐ์ง ์์ ์ค์ dig๋ฅผ ๊ฐ์ ์ข์ ํ์ง๋ง, ์๋์ ๊ฒ์ ์ ์ด ๋์
D3 Gallery์๋ ๋ ๋ง์ ์์ ๊ฐ ์๋ค. ๋ง์ฝ์ ๋ฐฐ์ฐ๊ธธ ์ํ๋ค๋ฉด Scott Murrayโs D3 Tutorials์ D3 documentation์ ์ด์ฉํ๊ธธ ๋ฐ๋๋ค.