5️⃣

05μž₯ D3.jsλ₯Ό μ‚¬μš©ν•œ 데이터 μ‹œκ°ν™”

 

5.1 D3.js κ°œμš”

5.1.1 D3.jsλŠ” 무엇인가?

  • D3.js(μ΄ν•˜ D3)λŠ” 데이터λ₯Ό μ‹œκ°ν™”ν•˜κΈ° μœ„ν•œ 무료 μ˜€ν”ˆ μ†ŒμŠ€ μžλ°”μŠ€ν¬λ¦½νŠΈ λΌμ΄λΈŒλŸ¬λ¦¬μ΄λ‹€. D3λŠ” μ›Ή ν‘œμ€€μ— κΈ°λ°˜ν•œ μ €μˆ˜μ€€ μ ‘κ·Ό 방식을 νƒν•˜κ³  있으며, μ΄λŠ” 동적이고 데이터 기반의 κ·Έλž˜ν”½μ„ μž‘μ„±ν•˜λŠ” 데 μžˆμ–΄ μœ μ—°ν•¨μ„ μ œκ³΅ν•œλ‹€.
 

5.1.2 D3 is a low level toolbox

  • D3λŠ” 전톡적인 차트 λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μ•„λ‹ˆλ‹€. μ°¨νŠΈλΌλŠ” κ°œλ…μ΄λΌκΈ°λ³΄λ‹€λŠ” λ‹€μŒκ³Ό 같은 κ΅¬μ„± μš”μ†Œλ“€λ‘œ λ°μ΄ν„°λ₯Ό μ‹œκ°ν™”ν•œλ‹€.
 

5.1.3 D3 κ΅¬μ„±μš”μ†Œ

stacked area chartλ₯Ό λ§Œλ“œλŠ” 예제λ₯Ό κΈ°μ€€μœΌλ‘œ ν•„μš”ν•œ κ΅¬μ„±μš”μ†ŒλŠ” λ‹€μŒκ³Ό κ°™λ‹€.
  • axes for documenting the position encodings, and
μ΄κ²ƒλ§Œ 보면 κ³ λ € 사항이 λ§Žμ€ κ²ƒμ²˜λŸΌ 보인닀. ν•˜μ§€λ§Œ 각 μš”μ†ŒλŠ” λ…λ¦½μ μœΌλ‘œ μ‚¬μš©ν•  수 μžˆμœΌλ―€λ‘œ λͺ¨λ“  것을 ν•œκΊΌλ²ˆμ— 배울 ν•„μš”λŠ” μ—†κ³  κ°œλ³„ μš”μ†Œλ₯Ό 배운 λ‹€μŒ μ‘°ν•©ν•΄μ„œ μ‚¬μš©ν•˜λ©΄ λœλ‹€.
(D3λŠ” κ±°λŒ€ν•œ 단일 ꡬ쑰가 μ•„λ‹ˆλΌ 30개의 κ°œλ³„ λͺ¨λ“ˆ λͺ¨μŒμ΄λ‹€!)
 

5.1.4 D3의 νŠΉμ§•

  • D3λŠ” μœ μ—°ν•˜λ‹€
    • D3의 λͺ¨λ“  μš”μ†ŒλŠ” μ™„μ „νžˆ μ œμ–΄κ°€ κ°€λŠ₯ν•˜λ©° μ›ν•˜λŠ” λŒ€λ‘œ μ‹œκ°ν™”λ₯Ό ν•  수 μžˆλ‹€. (D3μ—λŠ” 데이터λ₯Ό λ‚˜νƒ€λ‚΄λŠ” 기본적인 ν‘œν˜„μ΄ μ—†μœΌλ©° 였직 μ‚¬μš©μžκ°€ 직접 μž‘μ„±ν•œ μ½”λ“œλ§Œμ΄ μ‘΄μž¬ν•œλ‹€.)
  • D3λŠ” μ›Ήκ³Ό ν•¨κ»˜ λ™μž‘ν•œλ‹€
    • D3λŠ” μƒˆλ‘œμš΄ κ·Έλž˜ν”½ ν‘œν˜„μ„ λ„μž…ν•˜μ§€ μ•Šκ³  SVG 및 Canvas와 같은 μ›Ή ν‘œμ€€μ„ 직접 μ‚¬μš©ν•˜μ—¬ D3λ₯Ό ν™œμš©ν•œλ‹€.
  • D3λŠ” λ§žμΆ€ν˜• μ‹œκ°ν™”λ₯Ό μœ„ν•œ 도ꡬ이닀
  • D3λŠ” 동적 μ‹œκ°ν™”λ₯Ό μœ„ν•œ 도ꡬ이닀
    • D3λŠ” 동적 λ Œλ”λ§μ„ μ§€μ›ν•œλ‹€. λ”°λΌμ„œ D3λŠ” 데이터 배열을 기반으둜 ν•˜μ—¬ μ‘°κ±΄λΆ€λ‘œ HTML μš”μ†Œλ₯Ό μƒμ„±ν•˜κ³  λ Œλ”λ§ν•  수 μžˆλ‹€.
 

5.1.5 D3와 ReactλŠ” 악어와 μ•…μ–΄μƒˆ 관계이닀.

  • D3와 ReactλŠ” 악어와 μ•…μ–΄μƒˆμ²˜λŸΌ μ„œλ‘œμ—κ²Œ μžˆμ–΄ 이읡을 μ£Όκ³ λ°›λŠ” 상리곡생 관계에 λ†“μ—¬ μžˆλ‹€.
  • μ»΄ν¬λ„ŒνŠΈ 기반 μ•„ν‚€ν…μ²˜μ™€ 효율적인 가상 DOM λ Œλ”λ§μ΄λΌλŠ” νŠΉμ§•μ„ μ§€λ‹ˆλŠ” ReactλŠ” μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬μΆ•ν•˜λŠ” 반면, 데이터 기반 λ¬Έμ„œ(Data Driven Documents)인 D3λŠ” 데이터 기반의 직접적인 DOM μ‘°μž‘μ„ 톡해 μ •κ΅ν•˜κ³  μœ μ—°ν•œ μ‹œκ°ν™”λ₯Ό κ΅¬μΆ•ν•œλ‹€.
  • μ΄λŸ¬ν•œ λ¦¬μ•‘νŠΈμ™€ D3λŠ” ν†΅ν•©λ˜λ©΄ μ„œλ‘œμ˜ 강점을 μ‚΄λ €μ„œ interactiveν•œ λ°μ΄ν„° μ‹œκ°ν™”μ— 크게 κΈ°μ—¬ν•  수 μžˆλ‹€. μ™œλƒν•˜λ©΄ λ¦¬μ•‘νŠΈ μ»΄ν¬λ„ŒνŠΈλŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μƒνƒœμ™€ UI λ‘œμ§μ„ κ΄€λ¦¬ν•˜κ³ , D3λŠ” μ‹œκ°μ  μš”μ†Œμ˜ μ •ν™•ν•œ λ Œλ”λ§κ³Ό μ• λ‹ˆλ©”μ΄μ…˜μ„ μ²˜λ¦¬ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€. 즉, λͺ…ν™•ν•œ μ±…μž„ ꡬ뢄을 톡해 κ°œλ°œμžλŠ” λ°˜μ‘μ„±μ΄ λ›°μ–΄λ‚˜λ©° μœ μ§€ 관리 μš©μ΄ν•œ 데이터 기반 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ ꡬ좕할 수 있게 λ˜λŠ” 것이닀.
  • μ‹€μ œλ‘œ, 데이터λ₯Ό κ°€μ Έμ˜€κ³  λ‚˜μ„œ React μ»΄ν¬λ„ŒνŠΈ λ‚΄μ—μ„œ 이 데이터λ₯Ό μ²˜λ¦¬ν•˜κ²Œ λœλ‹€. μ€€λΉ„κ°€ μ™„λ£Œλ˜λ©΄ ν•¨μˆ˜ν˜• μ»΄ν¬λ„ŒνŠΈμ˜ useEffect와 κ°™μ€ React의 생λͺ… μ£ΌκΈ° λ©”μ„œλ“œ λ‚΄μ—μ„œ D3 λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ–΄ SVG μš”μ†Œλ₯Ό μƒμ„±ν•˜κ³  μ—…λ°μ΄νŠΈν•œλ‹€. μ΄λŸ¬ν•œ 과정을 톡해 ν•„μš”ν•œ κ²½μš°μ—λ§Œ D3 μ‘°μž‘μ΄ μˆ˜ν–‰λ˜λ―€λ‘œ React의 가상 DOM의 μ„±λŠ₯상 이점을 보쑴할 수 μžˆλ‹€.
 

5.2 D3 μ‹œμž‘ν•˜κΈ°

5.2.1 μ„€μΉ˜ 방법

Nodeλ₯Ό μ΄μš©ν•œ μ›Ή μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ°œλ°œν•˜λŠ” 경우 yarn, npm, pnpmκ³Ό 같은 νŒ¨ν‚€μ§€ λ§€λ‹ˆμ €λ₯Ό 톡해 D3λ₯Ό μ„€μΉ˜ν•  수 μžˆλ‹€.
# npm npm install d3 # yarn yarn add d3 # pnpm pnpm add d3
λ‹€μŒ ꡬ문을 톡해 D3λ₯Ό λ‘œλ“œν•  수 μžˆλ‹€
import * as d3 from 'd3';
λ³Έ ν”„λ‘œμ νŠΈλŠ” npm을 기반으둜 μ„€λͺ…ν•˜κ² λ‹€.
 

5.2.2 D3 μ΄μš©μ— ν•„μš”ν•œ μš”μ†Œ 및 μ£Όμ˜ν•  점

  • svg
λΈŒλΌμš°μ €μƒμ—μ„œ 데이터λ₯Ό μ‹œκ°ν™”ν•˜λŠ” 경우, μš°λ¦¬λŠ” 보톡 SVG μš”μ†Œλ“€λ‘œ μž‘μ—…ν•œλ‹€. (SVGλŠ” ν‘œν˜„λ ₯이 λ›°μ–΄λ‚˜κ³  μœ„μΉ˜κ°€ ν™•μ‹€ν•˜κ²Œ μž‘ν˜€μžˆκΈ° λ•Œλ¬Έμ΄λ‹€)
πŸ’‘
SVG (Scalable Vector Graphics)λž€, 벑터 κ·Έλž˜ν”½μ„ μ„œμˆ ν•˜λŠ” XML 기반의 λ§ˆν¬μ—… 언어이닀. ν…μŠ€νŠΈ 기반의 μ—΄λ¦° μ›Ή ν‘œμ€€ 쀑 ν•˜λ‚˜μ΄λ©° λͺ¨λ“  μ‚¬μ΄μ¦ˆμ—μ„œ κΉ”λ”ν•˜κ²Œ λ Œλ”λ§ λ˜λŠ” 이미지λ₯Ό μ„œμˆ ν•œλ‹€. (ν™•λŒ€ 및 μΆ•μ†Œλ₯Ό ν•˜κ±°λ‚˜ νšŒμ „ν•˜μ—¬λ„ 이미지 손상이 μΌμ–΄λ‚˜μ§€ μ•ŠλŠ”λ‹€.) 즉, SVGλŠ” HTMLκ³Ό ν…μŠ€νŠΈμ˜ 관계λ₯Ό κ·Έλž˜ν”½ 상에 μ μš©ν•œ 것이닀.
 
  • useRef
svg μš”μ†Œ(DOM μš”μ†Œ)에 λŒ€ν•œ μ°Έμ‘°λ₯Ό μ €μž₯ν•˜κΈ° μœ„ν•΄ μ‚¬μš©
 
  • useEffect
D3.js μ½”λ“œλ₯Ό React λ Œλ”λ§ 주기에 맞좰 μ‹€ν–‰ν•˜κ³ , 데이터 변경에 따라 μ‹œκ°ν™”λ₯Ό μ—…λ°μ΄νŠΈν•˜κΈ° μœ„ν•΄μ„œ useEffectλ₯Ό μ‚¬μš©ν•œλ‹€. useEffectλŠ” μ»΄ν¬λ„ŒνŠΈκ°€ λ Œλ”λ§ 된 후에 μ‹€ν–‰λ˜λ―€λ‘œ, DOM μš”μ†Œκ°€ μ‘΄μž¬ν•˜λŠ” μ‹œμ μ— D3 μ½”λ“œλ₯Ό μ‹€ν–‰ν•  수 μžˆλ‹€. useEffect 밖에 d3.select κ΄€λ ¨ μ½”λ“œλ₯Ό 두면, 아직 DOM μš”μ†Œκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ‹œμ μ— D3 μ½”λ“œκ°€ μ‹€ν–‰λ˜μ–΄ 아무것도 λ Œλ”λ§ λ˜μ§€ μ•ŠλŠ”λ‹€. (React의 λ Œλ”λ§ 사이클과 λ§žμ§€ μ•ŠκΈ° λ•Œλ¬Έμ— κ²°κ³Όκ°€ λ Œλ”λ§ λ˜μ§€ μ•ŠμŒ)
 
  • ν”„λ‘œμ νŠΈ κΈ°λ³Έ ꡬ성
import { useEffect, useRef } from 'react'; import * as d3 from 'd3'; const ComponentName = () => { const ref = useRef; useEffect(() => { const svg = d3.select(ref.current); /* 데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ μ‹œκ°ν™”ν•˜κΈ° μœ„ν•œ D3 μ½”λ“œ μž‘μ„± */ },[]) } export default ComponentName;
 

5.3 D3둜 μ‹œκ°ν™” κ΅¬μ„±ν•˜κΈ°

5.3.1 μ‹œκ°ν™” ꡬ성 μ‹œμž‘ν•˜κΈ°

  • SVG μš”μ†Œ λ§Œλ“€κΈ°
D3λ₯Ό μ‹œμž‘ν•˜κΈ° μœ„ν•΄ κ°„λ‹¨ν•œ <svg> μš”μ†Œλ₯Ό λ Œλ”λ§ ν•΄λ³΄μž. width, height 및 backgroundColorλ₯Ό μΆ”κ°€ν•˜μ—¬ μš°λ¦¬λŠ” <svg>μš”μ†Œλ₯Ό μƒμ„±ν•œ 것을 확인할 수 μžˆλ‹€.
const Svg1 = () => { return ( <> <svg style={{ width: "100px", height: "100px", backgroundColor: "red" }} /> </> ); }; export default Svg1;
notion image
 
μ΄λ²ˆμ—λŠ” 원을 그렀보자.
notion image
λ¦¬μ•‘νŠΈλ₯Ό μ΄μš©ν•΄μ„œ 원은 λ‹€μŒκ³Ό 같이 λ‚˜νƒ€λ‚Ό 수 μžˆλ‹€.
import { useEffect, useRef } from "react"; import * as d3 from "d3"; const Svg2 = () => { const ref = useRef(); // 1) useEffect(() => { // 2) const svgElement = d3.select(ref.current); // 3) svgElement .append("circle") // 4) .attr("cx", 100) .attr("cy", 80) .attr("r", 50); }, []); return ( <> {/* 1 */} <svg ref={ref} /> </> ); }; export default Svg2;
  1. useRefλ₯Ό μ‚¬μš©ν•΄μ„œ λ Œλ”λ§λœ <svg> μš”μ†Œμ— λŒ€ν•œ μ°Έμ‘°λ₯Ό μ €μž₯ν•œλ‹€.
  1. μ»΄ν¬λ„ŒνŠΈκ°€ 마운트될 λ•Œ d3 μ½”λ“œλ₯Ό μ‹€ν–‰μ‹œν‚¨λ‹€.
  1. d3.select()λ₯Ό μ΄μš©ν•˜μ—¬ refλ₯Ό d3 selection object둜 λ°”κΎΌλ‹€.
  1. d3 selection objectλ₯Ό μ‚¬μš©ν•΄μ„œ <circle> μš”μ†Œλ₯Ό μΆ”κ°€ν•œλ‹€.
 
μœ„ μ½”λ“œλŠ” ν•˜λ‚˜μ˜ 원을 그리기 μœ„ν•΄ λ§Žμ€ μ–‘μ˜ μ½”λ“œλ₯Ό μ‚¬μš©ν•˜κ³  μžˆλ‹€. μ‹€μ œλ‘œλŠ” μ•„λž˜μ™€ 같이 κ°„λ‹¨ν•˜κ²Œ μž‘μ„±ν•˜λ”λΌλ„ λ˜‘κ°™μ€ 결과물을 λ„μΆœν•΄ λ‚Ό 수 μžˆλ‹€.
notion image
const Svg3 = () => { return ( <> <svg> <circle cx="100" cy="80" r="50"></circle> </svg> </> ); }; export default Svg3;
ν•˜μ§€λ§Œ μš°λ¦¬λŠ” ν•™μŠ΅μ˜ 단계에 있기 λ•Œλ¬Έμ— 2번의 방법을 주둜 μ‚¬μš©ν•΄μ„œ μ½”λ“œλ₯Ό κ΅¬μ„±ν•˜κΈ°λ‘œ ν•œλ‹€.
 

5.3.2 D3 Architecture flow

D3λŠ” λ‹€μŒκ³Ό 같은 νλ¦„λŒ€λ‘œ λ™μž‘ν•œλ‹€.
  1. μ™ΈλΆ€ 파일(JSON, CSV λ“±)λ‘œλΆ€ν„° 데이터λ₯Ό λΆˆλŸ¬μ˜€κ±°λ‚˜ JS array 및 object ν˜•νƒœλ‘œ 데이터λ₯Ό μ œκ³΅ν•œλ‹€.
  1. CSS selector ν˜Ήμ€ useRef등을 μ‚¬μš©ν•΄μ„œ HTML, SVG μš”μ†Œλ₯Ό μ„ νƒν•œλ‹€.
  1. μ„ νƒλœ DOM듀을 각 데이터에 λ§žμΆ”μ–΄μ„œ μ‹œκ°ν™”λ  수 μžˆλ„λ‘ μ‘°μž‘μ„ κ°€ν•œλ‹€.
notion image
 

5.3.3 μ‹œκ°ν™” κ΅¬μ„±μš”μ†Œ

1. selection

  • D3λŠ” λ©”μ„œλ“œ 체이닝을 μ΄μš©ν•˜μ—¬ svgλ₯Ό 닀룬닀.
    • D3의 selection은 selection 객체λ₯Ό λ°˜ν™˜ν•˜λŠ”λ°, λ©”μ„œλ“œ 체이닝을 톡해 μ„ νƒλœ DOM μš”μ†Œμ— attribute와 style 등을 μ μš©ν•  수 있게 도와쀀닀. β†’ 이λ₯Ό 톡해 데이터 μ‘°μž‘κ³Ό μ‹œκ°ν™”λ₯Ό 효율적으둜 μ§„ν–‰ν•  수 있게 λœλ‹€.
 
μ˜ˆμ‹œλ₯Ό 톡해 selection이 μ–΄λ–»κ²Œ λ™μž‘ν•˜λŠ”μ§€ μ‚΄νŽ΄λ³΄μž.
import { useEffect, useRef } from "react"; import * as d3 from "d3"; const Selection = () => { const ref = useRef(); useEffect(() => { const svg = d3.select(ref.current); svg.append("circle") .data() // option .attr("cx", 100) .attr("cy", 80) .attr("r", 50); }, []); return <svg ref={ref} width={200} height={200} />; }; export default Selection;
  1. selectλ₯Ό μ΄μš©ν•΄μ„œ Elementλ₯Ό νƒμƒ‰ν•œ λ‹€μŒ selection 객체λ₯Ό μƒμ„±ν•œλ‹€.
  1. dataκ°€ μžˆλ‹€λ©΄ 데이터λ₯Ό λ„£λŠ” κ²½μš°λ„ μžˆλ‹€
  1. appendλ₯Ό 톡해 circleμš”μ†Œλ₯Ό μΆ”κ°€ν•œλ‹€
  1. μƒμ„±λœ μš”μ†Œμ˜ cx, cy, r 값에 데이터λ₯Ό λ„£λŠ”λ‹€.
 
이λ₯Ό 톡해 selectλŠ” κ°€μž₯ μ„ ν–‰λ˜μ–΄μ•Ό ν•˜λŠ” ν–‰μœ„μž„μ„ μ§μž‘ν•  수 μžˆλ‹€.
selection에 λŒ€ν•΄ 더 μžμ„Ένžˆ μ•Œκ³ μ‹Άλ‹€λ©΄ μ•„λž˜ λ‚΄μš©μ„ μ°Έκ³ ν•˜λ©΄ μ’‹λ‹€.
 

2. axis

μΆ•(axis) μ»΄ν¬λ„ŒνŠΈλŠ” SVG μ»¨ν…Œμ΄λ„ˆ(일반적으둜 단일 g μš”μ†Œ)의 선택 ν›„ ν˜ΈμΆœν•˜λ©΄ 좕을 μ±„μšΈ 수 μžˆλ‹€.
notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; const Axis = () => { const ref = useRef(); useEffect(() => { const svg = d3.select(ref.current) .attr("width", 1000) .attr("height", 200); const x = d3.scaleLinear() .domain([1, 10]) .range([1,500]); svg .append("g") .attr("transform", "translate(0,100)") .call(d3.axisBottom(x)); }, []); return <svg ref={ref} />; }; export default Axis;
 
  • μΆ• 생성 및 μΆ”κ°€
svg.append("g") .attr("transform", "translate(0,100)") .call(d3.axisBottom(x));
  1. SVG μš”μ†Œμ— κ·Έλ£Ή μš”μ†Œ(g)λ₯Ό μΆ”κ°€ν•œλ‹€.
  1. transform 속성을 μ‚¬μš©ν•˜μ—¬ yμΆ• λ°©ν–₯으둜 100 ν”½μ…€ μ΄λ™μ‹œν‚¨λ‹€.
  1. d3.axisBottom(x)을 ν˜ΈμΆœν•˜μ—¬ μˆ˜ν‰ 좕을 μƒμ„±ν•œλ‹€.
 

3. scale

scale을 μ‚¬μš©ν•˜λ©΄ range와 domain을 μ§€μ •ν•¨μœΌλ‘œμ¨ ν˜•νƒœμ™€ 크기λ₯Ό μ›ν•˜λŠ” λŒ€λ‘œ λ§Œλ“€ 수 μžˆλ‹€.
notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; const Scale = () => { const ref = useRef(); useEffect(() => { // SVG 크기 μ„€μ • const width = 2000; const height = 200; // 데이터 (0 ~ 10,000) const data = Array.from({ length: 10000 }, (_, i) => i + 1); // SVG μš”μ†Œ 선택 const svg = d3 .select(ref.current) .attr("width", width) .attr("height", height); // x μŠ€μΌ€μΌ μ •μ˜ (μ„ ν˜• μŠ€μΌ€μΌ) const xScale = d3 .scaleLinear() .domain([0, d3.max(data)]) // μž…λ ₯ 도메인: λ°μ΄ν„°μ˜ μ΅œμ†Œκ°’κ³Ό μ΅œλŒ€κ°’ .range([0,1000]); // 좜λ ₯ λ²”μœ„: SVG의 λ„ˆλΉ„ // xμΆ• μΆ”κ°€ const xAxis = d3.axisBottom(xScale); svg .append("g") .attr("transform", `translate(0, ${height - 100})`) .call(xAxis); }, []); return ( <> <svg ref={ref}></svg> </> ); }; export default Scale;
  • d3.scaleLinear
    • (μœ„ μ½”λ“œμƒ) xScaleLinear λΌλŠ” λ³€μˆ˜μ— d3.scaleLinear() ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μ—¬ μž…λ ₯ λ²”μœ„(domain)λ₯Ό 좜λ ₯ λ²”μœ„(range)에 λ§€ν•‘ν•œλ‹€. 그리고 이 λ‘˜κ°„μ˜ μ„ ν˜• 관계λ₯Ό κ°€μ§€λŠ” scale을 μƒμ„±ν•œλ‹€.
    • notion image
    • κΈ°λ³ΈκΌ΄
    • scaleLinear(domain, range)
 
  • linear.domain([μ΅œμ†Ÿκ°’, μ΅œλŒ“κ°’])
    • domain은 μž…λ ₯ λ°μ΄ν„°μ˜ λ²”μœ„λ₯Ό μ§€μ •ν•˜λŠ” μž…λ ₯ λ°μ΄ν„°μ˜ 집합이닀. (μ‹œκ°ν™”ν•˜λ €λŠ” λ°μ΄ν„°μ˜ μ΅œμ†Ÿκ°’κ³Ό μ΅œλŒ“κ°’)
    • κΈ°λ³ΈκΌ΄
    • const xScale = d3 .scaleLinear() .domain([μ΅œμ†Ÿκ°’, μ΅œλŒ“κ°’])
 
  • linear.range([μ΅œμ†Ÿκ°’, μ΅œλŒ“κ°’])
    • rangeλŠ” 좜λ ₯ λ²”μœ„λ₯Ό μ§€μ •ν•˜λŠ” 좜λ ₯ κ°’μ˜ 집합이닀.
    • κΈ°λ³Έ κΌ΄
      • const xScale = d3 .scaleLinear() .domain([μ΅œμ†Ÿκ°’, μ΅œλŒ“κ°’]) .range([μ΅œμ†Ÿκ°’, μ΅œλŒ“κ°’]
    • range 비ꡐ
      • μ•žμ„œ μ‚΄νŽ΄λ³Έ rangeλ₯Ό 1000 β†’ 2000으둜 λ°”κΎΈλ©΄ λ‹€μŒκ³Ό 같은 κ²°κ³Όκ°€ λ‚˜μ˜¨λ‹€. λ§€ν•‘λœ 값이 이전과 달라진 것을 확인할 수 μžˆλ‹€. (μ‹€μ œλ‘œ 화면상 길이도 2배정도 λŠ˜μ–΄λ‚œ 것을 λ³Ό 수 μžˆλ‹€.)
        notion image
 

4. color

μ•žμ„œ μ‚΄νŽ΄λ³΄μ•˜λ˜ 원 그리기 μ˜ˆμ œμ—μ„œ colorλ₯Ό μ±„μš°κΈ° μœ„ν•΄μ„œλŠ” fill 속성을 μ΄μš©ν•˜κ³  색상을 μ§€μ •ν•΄ μ£Όλ©΄ λœλ‹€
notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; const Color = () => { const ref = useRef(); useEffect(() => { const svg= d3.select(ref.current); const color = d3.color('steelblue'); svg .append("circle") .attr("cx", 100) .attr("cy", 80) .attr("r", 50) .attr("fill", color); // 색상 μ§€μ • }, []); return ( <> <svg ref={ref} /> </> ); }; export default Color;
 

5. transition

μ΄λ²ˆμ—λŠ” transition을 μ΄μš©ν•΄μ„œ ν™”λ©΄μƒμ˜ 원이 μ™”λ‹€κ°”λ‹€ν•˜λŠ” 것을 κ΅¬ν˜„ν•΄λ³΄μž.
D3μ—μ„œ νŠΈλžœμ§€μ…˜μ€ μš”μ†Œμ˜ μ†μ„±μ΄λ‚˜ μŠ€νƒ€μΌμ„ λΆ€λ“œλŸ½κ²Œ λ³€ν™”μ‹œν‚€λŠ” 방법이닀.
μ•„λž˜μ˜ μ½”λ“œμ—μ„œλŠ” 원(circle)의 λ°˜μ§€λ¦„(r)κ³Ό 색상(fill)에 νŠΈλžœμ§€μ…˜μ„ μ μš©ν•˜κ³  μžˆλ‹€.
notion image
notion image
import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; const generateCircles = () => { return Array.from({ length: 10 }, () => Math.floor(Math.random() * 10)); }; const Transition = () => { const [visibleCircles, setVisibleCircles] = useState(generateCircles()); const ref = useRef(); useEffect(() => { const timer = setInterval(() => { setVisibleCircles(generateCircles()); }, 2000); return () => clearInterval(timer); }, []); useEffect(() => { const svg = d3.select(ref.current); svg .selectAll("circle") .data(visibleCircles, (d) => d) .join( (enter) => enter .append("circle") .attr("cx", (d) => d * 10 + 5) .attr("cy", 10) .attr("r", 0) .attr("fill", "cornflowerblue") .transition() .duration(1000) .attr("r", 5), (update) => update.attr("fill", "lightgrey"), (exit) => exit.transition().duration(1000).attr("r", 0).remove() ); }, [visibleCircles]); return <svg viewBox="0 0 100 20" ref={ref} />; }; export default Transition;
체크 포인트
  • enter tarnsition
enter.append("circle") .attr("cx", d => d * 10 + 5) .attr("cy", 10) .attr("r", 0) .attr("fill", "cornflowerblue") .transition() .duration(1000) .attr("r", 5)
μƒˆλ‘œ μƒμ„±λ˜λŠ” 원은 λ°˜μ§€λ¦„ 0μ—μ„œ μ‹œμž‘ν•΄ 1초(1000ms) λ™μ•ˆ λ°˜μ§€λ¦„ 5둜 μ»€μ§€λŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•˜κ³ μžˆλ‹€.
 
  • update transition
update.attr("fill", "lightgrey")
κΈ°μ‘΄ μ›μ˜ 색상을 μ¦‰μ‹œ λ³€κ²½ν•œλ‹€.
이 μ½”λ“œκ°€ μ—†λ‹€λ©΄ lightgrey둜 원이 λ³€ν•˜μ§€ μ•ŠλŠ”λ‹€.
 
  • exit transition
exit.transition().duration(1000) .attr("r", 0) .remove()
μ‚­μ œλ  원은 1초 λ™μ•ˆ λ°˜μ§€λ¦„μ΄ 0으둜 μ€„μ–΄λ“œλŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•œ ν›„ μ œκ±°λœλ‹€.
 
useEffect(() => { const timer = setInterval(() => { setVisibleCircles(generateCircles()); }, 2000); return () => clearInterval(timer); }, []);
이 useEffectλŠ” μ»΄ν¬λ„ŒνŠΈκ°€ 마운트될 λ•Œ ν•œ 번만 μ‹€ν–‰λ˜λ©°, 2μ΄ˆλ§ˆλ‹€ visibleCircles μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•œλ‹€. (μ»΄ν¬λ„ŒνŠΈκ°€ μ–Έλ§ˆμš΄νŠΈλ  λ•Œ μΈν„°λ²Œμ„ μ •λ¦¬ν•˜λŠ” 클린업 ν•¨μˆ˜λ„ ν¬ν•¨μ‹œμΌœ μœ„ν—˜μ„±μ„ μ œκ±°ν•˜μ˜€λ‹€.)
 

5.3.3 D3 shape μ’…λ₯˜

1. Lines

notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; import data from './data.js' const LineShape = () => { const ref = useRef(); useEffect(() => { const width = 1000; const height = 400; const margin = { top:60, right: 40, bottom: 60, left: 60 }; const svg = d3.select(ref.current) .attr("width", width) .attr("height", height) const x = d3.scaleTime() // scaleTime: xμΆ• μ‹œκ°„ λ‚˜νƒ€λƒ„ .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)]) .range([height - margin.bottom, margin.top]); // 라인 그리기 - line λ©”μ„œλ“œ, path μ‚¬μš© const line = d3.line() .x(d => x(d.date)) .y(d => y(d.value)); svg.append("path") .datum(data) .attr("fill", "none") .attr("stroke", "steelblue") .attr("stroke-width", 2) .attr("d", line); const xAxis = d3.axisBottom(x) .ticks(d3.timeMonth.every(1)) .tickSizeOuter(0) .tickFormat(d3.timeFormat("%mμ›”")); // ν•œκ΅­μ–΄ λ³€ν™˜ svg.append("g") .attr("transform", `translate(0,${height - margin.bottom})`) .call(xAxis) .append("text") // xμΆ• λ ˆμ΄λΈ” ν…μŠ€νŠΈ (λ²”λ‘€) .attr("fill", "red") .attr("x", width/2) .attr("y", margin.bottom - 10) .style("font-size", "12px") .text("μ›”"); svg.append("g") .attr("transform", `translate(${margin.left},0)`) .call(d3.axisLeft(y)) // yμΆ• λ ˆμ΄λΈ” ν…μŠ€νŠΈ (λ²”λ‘€) .append("text") .attr("fill", "green") .attr("x", -margin.left-1) .attr("y", height/2) .attr("text-anchor", "start") .style("font-size", "12px") .text("인원 수"); // 차트 제λͺ© svg.append("text") .attr("x", width / 2) .attr("y", margin.top / 2 +10) .attr("text-anchor", "middle") .style("font-size", "20px") .style("font-weight", "bold") .text("월별 λ„μ„œ λŒ€μ—¬ μ‹ μ²­μž 수"); }, []); return <svg ref={ref}/> }; export default LineShape;
 
  • scale μ •μ˜
const x = d3.scaleTime() // scaleTime: xμΆ• μ‹œκ°„ λ‚˜νƒ€λƒ„ .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)]) .range([height - margin.bottom, margin.top]);
 
  • μ„  그리기
const line = d3.line() .x(d => x(d.date)) .y(d => y(d.value)); svg.append("path") .datum(data) .attr("fill", "none") .attr("stroke", "steelblue") .attr("stroke-width", 2) .attr("d", line);
라인 차트λ₯Ό λ§Œλ“€κΈ° μœ„ν•΄ μ„  그리기 λ©”μ„œλ“œμΈ line을 μ‚¬μš©ν•˜μ˜€λ‹€. λ˜ν•œ x 속성은 d.date 값을 xμΆ• scale에 따라 λ§€ν•‘ν•˜μ˜€κ³ , y 속성은 d.value값을 yμΆ• scale에 따라 λ§€ν•‘ν•˜μ˜€λ‹€.
라인 차트λ₯Ό λ§Œλ“€κΈ° μœ„ν•΄ μ€‘μš”ν•œ path elementλ₯Ό svg에 μΆ”κ°€ν•˜κΈ°λ„ ν•˜μ˜€λ‹€. data 배열을 κΈ°μ€€μœΌλ‘œ 데이터λ₯Ό μ±„μ›Œ λ„£μ—ˆκ³ , μˆœμ„œλŒ€λ‘œ μ±„μš°κΈ° μ—†μŒ / μ„  색상 / μ„  λ‘κ»˜ 섀정을 ν•˜μ˜€λ‹€. λ§ˆμ§€λ§‰μœΌλ‘œ line λ©”μ„œλ“œλ₯Ό μ΄μš©ν•΄μ„œ 데이터 point듀을 μ—°κ²° ν•˜μ˜€λ‹€.
  • 라인 μŠ€νƒ€μΌ
    • stroke, stroke-width, stroke-dasharray 등을 μ‚¬μš©ν•˜μ—¬ 라인 μŠ€νƒ€μΌμ„ λ³€κ²½ν•  수 μžˆλ‹€.
 

2. Bar

notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; import data from "./data.js"; const BarShape = () => { const ref = useRef(); useEffect(() => { const width = 1000; const height = 400; const margin = { top: 50, right: 30, bottom: 40, left: 40 }; const svg = d3 .select(ref.current) .attr("width", width) .attr("height", height); // x, y μΆ• μŠ€μΌ€μΌ μ •μ˜ const xScale = d3 .scaleBand() .domain(data.map((d) => d.name)) .range([margin.left, width - margin.right]) .padding(0.25); const yScale = d3 .scaleLinear() .domain([0, d3.max(data, (d) => d.value)]) .nice() .range([height - margin.bottom, margin.top]); svg.selectAll("*").remove(); // Bar 생성 svg .append("g") .attr("fill", "#5ED3F3") .selectAll("rect") .data(data) .join("rect") .attr("x", (d) => xScale(d.name)) .attr("y", (d) => yScale(d.value)) .attr("height", (d) => yScale(0) - yScale(d.value)) .attr("width", xScale.bandwidth()); // μΆ• 생성 svg .append("g") .call(d3.axisLeft(yScale)) .attr("transform", `translate(${margin.left},0)`); svg .append("g") .call(d3.axisBottom(xScale).tickSizeOuter(0)) .attr("transform", `translate(0,${height - margin.bottom})`); // 차트 제λͺ© svg .append("text") .attr("x", width / 2) .attr("y", margin.top / 2 - 3) .attr("text-anchor", "middle") .style("font-size", "20px") .style("font-weight", "bold") .text("뢀문별 인원 ν˜„ν™©"); }, [data]); return <svg ref={ref} />; }; export default BarShape;
 
  • x,yμΆ• μŠ€μΌ€μΌ μ •μ˜
const xScale = d3 .scaleBand() // xμΆ•μ˜ μŠ€μΌ€μΌμ„ μƒμ„±ν•˜λŠ” ν•¨μˆ˜ .domain(data.map((d) => d.name)) // x좕에 ν‘œμ‹œν•  데이터. map()을 μ‚¬μš©ν•˜μ—¬ name을 μΆ”μΆœν•œ 배열을 λ°˜ν™˜ .range([margin.left, width - margin.right]) .padding(0.25); // λ§‰λŒ€μ˜ 간격을 쑰절 const yScale = d3 .scaleLinear() .domain([0, d3.max(data, (d) => d.value)]) .nice() .range([height - margin.bottom, margin.top]);
 
  • 데이터 μ—…λ°μ΄νŠΈ 반영
svg.selectAll("*").remove();
이전 데이터λ₯Ό μ œκ±°ν•˜λŠ” 역할을 ν•΄μ„œ 데이터 μ—…λ°μ΄νŠΈλ₯Ό λ°˜μ˜ν•œλ‹€. (이것이 μ—†λ‹€λ©΄ 데이터λ₯Ό μ—…λ°μ΄νŠΈ ν–ˆμ„ λ•Œ λΈ”λŸ­μ΄ 겹쳐져 λ³΄μ΄λŠ” 것을 확인할 수 μžˆλ‹€. β†’ data의 맨 λ§ˆμ§€λ§‰ μš”μ†Œλ₯Ό μ§€μ› λ‹€κ°€ 차트λ₯Ό μ—…λ°μ΄νŠΈ ν•˜λ©΄μ„œ 확인해보기 λ°”λž€λ‹€.)
 
  • Bar 생성
svg .append("g") .attr("fill", "#5ED3F3") .selectAll("rect") .data(data) .join("rect") .attr("x", (d) => xScale(d.name)) .attr("y", (d) => yScale(d.value)) .attr("height", (d) => yScale(0) - yScale(d.value)) .attr("width", xScale.bandwidth());
  1. SVG에 κ·Έλ£Ή μš”μ†Œ <g> μΆ”κ°€
  1. λ§‰λŒ€ 색상 μ±„μš°κΈ° μ„€μ •
  1. κ·Έλ£Ήλ‚΄μ˜ λͺ¨λ“  <rect> μš”μ†Œλ₯Ό 선택
  1. 데이터 λ°°μ—΄ dataλ₯Ό κ°€μ Έμ™€μ„œ μ‚¬κ°ν˜•μ— 바인딩
  1. 각 λ§‰λŒ€μ˜ x 속성 μ§€μ •
  1. 각 λ§‰λŒ€μ˜ y 속성 μ§€μ •
  1. 각 λ§‰λŒ€μ˜ 높이 μ„€μ •
  1. 각 λ§‰λŒ€μ˜ λ„ˆλΉ„ μ„€μ •
 

3. Areas

notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; import data from "./data.js"; const AreaShape = () => { const ref = useRef(); useEffect(() => { const width = 1000; const height = 500; const margin = { top: 20, right: 30, bottom: 30, left: 50 }; const svg = d3 .select(ref.current) .attr("width", width) .attr("height", height); const xScale = d3 .scaleTime() .domain(d3.extent(data, (d) => d.date)) .range([margin.left + 5, width - margin.right]); // +5λŠ” .attr("transform", `translate(${margin.left+5},0)`)의 +5와 λ§žμΆ˜κ²ƒμž„ (κ·Έλž˜μ•Ό μΌμ§μ„ μœΌλ‘œ 수직, μˆ˜ν‰μ˜ 경계가 κ²ΉμΉ˜μ§€ μ•Šκ²Œλ¨) const yScale = d3 .scaleLinear() .domain([0, d3.max(data, (d) => d.close)]) .nice() .range([height - margin.bottom, margin.top]); const area = d3 .area() .x((d) => xScale(d.date)) .y0(yScale(0)) .y1((d) => yScale(d.close)); // μ˜μ—­ 차트 그리기 svg.append("path").attr("fill", "#5ED3F3").attr("d", area(data)); // XμΆ• svg .append("g") .attr("transform", `translate(0,${height - margin.bottom})`) .call( d3 .axisBottom(xScale) .ticks(d3.timeYear.every(1)) .tickSizeOuter(0) .tickFormat(d3.timeFormat("%Y")) // 연도 λ‹¨μœ„λ‘œ ν‘œμ‹œ ); // YμΆ• svg .append("g") .attr("transform", `translate(${margin.left + 5},0)`) .call(d3.axisLeft(yScale).ticks(6)) // YμΆ• λ ˆμ΄λΈ” μΆ”κ°€ .call((g) => g.select(".domain").remove()) // YμΆ• 라인 제거 .call((g) => g .selectAll(".tick line") .clone() .attr("x2", width - margin.left - margin.right) .attr("stroke-opacity", 0.05) ) // 보쑰선 μΆ”κ°€ .call((g) => g .append("text") .attr("x", -margin.left) .attr("y", 10) .attr("fill", "currentColor") .attr("text-anchor", "start") .text("d3 λ‹€μš΄λ‘œλ“œ 횟수") ); // YμΆ• 제λͺ© μ„€μ • }, []); return <svg ref={ref} />; }; export default AreaShape;
 
  • μ˜μ—­ generatorλ₯Ό λ§Œλ“ λ‹€
const area = d3.area() .x((d) => xScale(d.date)) .y0(yScale(0)) .y1((d) => yScale(d.close));
d3.area()λ₯Ό 톡해 μ˜μ—­ generatorλ₯Ό λ§Œλ“€κ³ 
λ°μ΄ν„°μ˜ date 값을 xμΆ• μŠ€μΌ€μΌ ν•¨μˆ˜ x에 λ§€ν•‘ν•˜μ—¬ x μ’Œν‘œλ₯Ό μ„€μ •ν•œλ‹€.
그리고 μ˜μ—­μ˜ ν•˜λ‹¨ 경계λ₯Ό yμΆ• μŠ€μΌ€μΌ ν•¨μˆ˜ y에 0을 μž…λ ₯ν•˜μ—¬ μ„€μ •ν•œλ‹€. (μ–‘μˆ˜μΌ 경우 좕을 κΈ°μ€€μœΌλ‘œ μœ„μͺ½μœΌλ‘œ 뜨게되고, 음수일 경우 좕을 κΈ°μ€€μœΌλ‘œ μ•„λž˜μͺ½κΉŒμ§€ νŒŒκ³ λ“ λ‹€.)
λ§ˆμ§€λ§‰μœΌλ‘œ λ°μ΄ν„°μ˜ close 값을 yμΆ• μŠ€μΌ€μΌ ν•¨μˆ˜ y에 λ§€ν•‘ν•˜μ—¬ μ˜μ—­μ˜ 상단 경계λ₯Ό μ„€μ •ν•œλ‹€.
 

4. Pies

notion image
import { useEffect, useRef } from "react"; import * as d3 from "d3"; import data from "./data.js"; const PieShape = () => { const ref = useRef(); useEffect(() => { // 차트 면적 ꡬ성 const width = 1000; const height = 500; // SVG container 생성 const svg = d3 .select(ref.current) .attr("width", width) .attr("height", height) .attr("viewBox", [-width / 2, -height / 2, width, height]); // 색상 척도 생성 const color = d3 .scaleOrdinal() .domain(data.map((d) => d.name)) .range(["red", "orange", "green", "blue", "yellow"]); // μ‚¬μš©μž μ •μ˜ 색상 λ°°μ—΄ // pie layoutκ³Ό arc generator 생성 const pie = d3.pie().value((d) => d.value); const arc = d3 .arc() .innerRadius(0) .outerRadius(height / 2); const labelRadius = arc.outerRadius()() * 0.6; // label별 arc generator const arcLabel = d3.arc().innerRadius(labelRadius).outerRadius(labelRadius); const arcs = pie(data); // 총합 계산 const total = d3.sum(data, (d) => d.value); // 타이틀 μΆ”κ°€ svg .append("text") .attr("x", -width / 2 + 20) // x 속성을 μ‘°μ •ν•˜μ—¬ μ™Όμͺ½μ— 배치 .attr("y", -height / 2 + 20) // y 속성을 μ‘°μ •ν•˜μ—¬ 상단에 배치 .attr("text-anchor", "start") // μ™Όμͺ½ μ •λ ¬ .attr("font-size", "24px") .attr("font-weight", "bold") .text("μŒμ‹ μ„ ν˜Έλ„"); // 파이 쑰각 μΆ”κ°€ svg .append("g") .attr("stroke", "white") .selectAll("path") .data(arcs) .join("path") .attr("fill", (d) => color(d.data.name)) .attr("d", arc) // 라벨 μΆ”κ°€ svg .append("g") .attr("text-anchor", "middle") .selectAll("text") .data(arcs) .join("text") .attr("transform", (d) => `translate(${arcLabel.centroid(d)})`) .call((text) => text .append("tspan") .attr("y", "-0.4em") .attr("font-weight", "bold") .text((d) => d.data.name) ) .call((text) => text .append("tspan") .attr("x", 0) .attr("y", "0.7em") .text((d) => `${((d.data.value / total) * 100).toFixed(2)}%`) ); }, []); return <svg ref={ref} />; }; export default PieShape;
차트 λœ―μ–΄λ³΄κΈ°
  • 차트 면적 ꡬ성
const width = 1000; const height = 500;
 
  • SVG μ»¨ν…Œμ΄λ„ˆ 생성
const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
 
  • 색상 척도 생성
const color = d3 .scaleOrdinal() .domain(data.map((d) => d.name)) .range(["red", "orange", "green", "blue", "yellow"]); // μ‚¬μš©μž μ •μ˜ 색상 λ°°μ—΄
 
  • pie layoutκ³Ό arc generator 생성
const pie = d3.pie() .sort(null) // 파이 쑰각을 μ •λ ¬ν•˜μ§€ μ•ŠμŒ -> μ—†μœΌλ©΄ 정렬됨 .value(d => d.value); // value 속성을 μ‚¬μš©ν•΄ 파이 쑰각의 크기 κ²°μ • const arc = d3.arc() .innerRadius(0) .outerRadius(height/2); const labelRadius = arc.outerRadius()() * 0.6; // 라벨을 파이 쑰각 μ€‘μ‹¬μ—μ„œ μ•½κ°„ μ•ˆμͺ½μ— μœ„μΉ˜μ‹œν‚€κΈ° μœ„ν•΄ μ™ΈλΆ€ λ°˜μ§€λ¦„μ˜ 60%둜 μ„€μ •
 
  • label별 arc generator
const arcLabel = d3.arc() .innerRadius(labelRadius) // 라벨의 λ‚΄λΆ€ λ°˜μ§€λ¦„ μ„€μ • .outerRadius(labelRadius); // 라벨 μ™ΈλΆ€ λ°˜μ§€λ¦„ μ„€μ • const arcs = pie(data); // 데이터λ₯Ό λ°”νƒ•μœΌλ‘œ 파이 생성
 
  • 총합 계산
const total = d3.sum(data, (d) => d.value);
 
  • 타이틀 μΆ”κ°€
svg .append("text") .attr("x", -width / 2 + 20) // x 속성을 μ‘°μ •ν•˜μ—¬ μ™Όμͺ½μ— 배치 .attr("y", -height / 2 + 20) // y 속성을 μ‘°μ •ν•˜μ—¬ 상단에 배치 .attr("text-anchor", "start") // μ™Όμͺ½ μ •λ ¬ .attr("font-size", "24px") .attr("font-weight", "bold") .text("μŒμ‹ μ„ ν˜Έλ„");
 
  • 파이 쑰각 μΆ”κ°€
svg .append("g") .attr("stroke", "white") .selectAll("path") .data(arcs) .join("path") .attr("fill", (d) => color(d.data.name)) .attr("d", arc)
  1. svg.append("g").attr("stroke", "white") β†’ μƒˆλ‘œμš΄ κ·Έλ£Ή μš”μ†Œλ₯Ό μΆ”κ°€ν•˜κ³ , 그룹의 경계선을 ν°μƒ‰μœΌλ‘œ μ„€μ •.
  1. .selectAll("path").data(arcs).join("path") β†’ arcs 데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ path μš”μ†Œλ₯Ό 생성.
  1. .attr("fill", (d) => color(d.data.name)) β†’ 각 파이 쑰각의 색상을 μ„€μ •.
  1. .attr("d", arc) β†’ 각 파이 쑰각의 경둜 데이터λ₯Ό μ„€μ •.
 
  • 라벨 μΆ”κ°€
svg .append("g") .attr("text-anchor", "middle") .selectAll("text") .data(arcs) .join("text") .attr("transform", (d) => `translate(${arcLabel.centroid(d)})`) .call((text) => text .append("tspan") .attr("y", "-0.4em") .attr("font-weight", "bold") .text((d) => d.data.name) ) .call((text) => text .append("tspan") .attr("x", 0) .attr("y", "0.7em") .text((d) => `${((d.data.value / total) * 100).toFixed(2)}%`) );
  1. svg.append("g").attr("text-anchor", "middle") β†’ μƒˆλ‘œμš΄ κ·Έλ£Ή μš”μ†Œλ₯Ό μΆ”κ°€ν•˜κ³ , ν…μŠ€νŠΈ 정렬을 κ°€μš΄λ°λ‘œ μ„€μ •.
  1. .selectAll("text").data(arcs).join("text") β†’ arcs 데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ text μš”μ†Œλ₯Ό 생성.
  1. .attr("transform", (d) =>translate(${arcLabel.centroid(d)})) β†’ 각 라벨을 파이 쑰각의 μ€‘μ‹¬μœΌλ‘œ 이동.
  1. .call((text) => text.append("tspan").attr("y", "-0.4em").attr("font-weight", "bold").text((d) => d.data.name)) β†’ 각 라벨에 이름을 μΆ”κ°€.
  1. .call((text) => text.append("tspan").attr("x", 0).attr("y", "0.7em").text((d) =>${((d.data.value / total) * 100).toFixed(2)}%)) β†’ 각 라벨에 λ°±λΆ„μœ¨μ„ μΆ”κ°€
 

5.4 μ‹€μ „ 예제

notion image
import React, { useEffect, useRef } from "react"; import * as d3 from "d3"; import data from "./data.js"; const colors = [ "#FF0000", "#800000", "#FF69B4", "#FFA500", "#32CD32", "#00BFFF", "#8A2BE2", "#0000FF", "#8B4513", "#006400", ]; const ActualEx1 = () => { const ref = useRef(null); useEffect(() => { const svg = d3.select(ref.current).attr("width", 1000).attr("height", 800); svg.selectAll("*").remove(); const margin = { top: 50, right: 50, bottom: 50, left: 50 }; const width = 1000 - margin.left - margin.right; const height = 600 - margin.top - margin.bottom; svg .append("text") .attr("x", width / 2 + margin.left) .attr("y", margin.top / 2) .attr("text-anchor", "middle") .style("font-size", "24px") .text("μ£Όμš” μ’…ν•©λͺ° μ•± TOP 10 μ‚¬μš©μž 좔이 (2019λ…„ ~ 2024λ…„)"); const chart = svg .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`); const x = d3 .scaleBand() .domain(data.map((d) => d.date)) .range([0, width]) .padding(0.1); chart .append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(x)) .selectAll("text") .attr("transform", "rotate(-45)") .style("text-anchor", "end"); const y = d3.scaleLinear().domain([0, 3500]).range([height, 0]); chart.append("g").call(d3.axisLeft(y).tickValues(d3.range(0, 3600, 200))); chart .append("g") .attr("class", "grid") .call(d3.axisLeft(y).tickSize(-width).tickFormat("")) .selectAll("line") .style("opacity", 0.1); chart.selectAll(".grid .domain").remove(); const line = d3 .line() .x((d) => x(d.date) + x.bandwidth() / 2) .y((d) => y(d.value)); Object.keys(data[0]) .filter((key) => key !== "date") .forEach((key, i) => { const appData = data.map((d) => ({ date: d.date, value: d[key] })); chart .append("path") .datum(appData) .attr("fill", "none") .attr("stroke", colors[i % colors.length]) .attr("stroke-width", 2) .attr("d", line); }); const legend = svg .append("g") .attr( "transform", `translate(${width + margin.left + 10}, ${margin.top})` ); Object.keys(data[0]) .filter((key) => key !== "date") .forEach((key, i) => { const legendRow = legend .append("g") .attr("transform", `translate(-50, ${i * 20})`); legendRow .append("rect") .attr("width", 10) .attr("height", 10) .attr("fill", colors[i % colors.length]); legendRow.append("text").attr("x", 12).attr("y", 10).text(key); }); const tooltip = d3 .select("body") .append("div") .attr("class", "tooltip") .style("opacity", 0) .style("position", "absolute") .style("background-color", "white") .style("border", "solid") .style("border-width", "1px") .style("border-radius", "5px") .style("padding", "10px"); Object.keys(data[0]) .filter((key) => key !== "date") .forEach((key, i) => { chart .selectAll(`.dot-${key}`) .data(data) .enter() .append("circle") .attr("class", `dot-${key}`) .attr("cx", (d) => x(d.date) + x.bandwidth() / 2) .attr("cy", (d) => y(d[key])) .attr("r", 5) .attr("fill", colors[i % colors.length]) .on("mouseover", (evt, d) => { const date = d.date; const values = Object.keys(d) .filter((key) => key !== "date") .map( (key, index) => `<div style="display: flex; align-items: center;"><div style="width: 10px; height: 10px; background-color: ${ colors[index % colors.length] }; margin-right: 5px;"></div>${key}: ${d[key]}</div>` ) .join(""); tooltip.transition().duration(200).style("opacity", 0.9); tooltip .html(`<strong>${date}</strong><br/><br/>${values}`) .style("left", evt.pageX + 5 + "px") .style("top", evt.pageY - 28 + "px"); }) .on("mouseout", () => { tooltip.transition().duration(500).style("opacity", 0); }); }); }, [data]); return <svg ref={ref} />; }; export default ActualEx1;
  • 차트 제λͺ© μΆ”κ°€
svg.append("text") .attr("x", width / 2 + margin.left) .attr("y", margin.top / 2) .attr("text-anchor", "middle") .style("font-size", "24px") .text("μ£Όμš” μ’…ν•©λͺ° μ•± TOP 10 μ‚¬μš©μž 좔이 (2019λ…„ ~ 2024λ…„)");
 
  • X,YμΆ• μ„€μ •
    • XμΆ• β†’ 각 연도λ₯Ό κ΅¬λΆ„ν•˜λŠ” Band Scale을 μ‚¬μš©ν•˜μ—¬ μ„€μ •ν•˜κ³ , ν…μŠ€νŠΈλ₯Ό 45도 νšŒμ „μ‹œμΌœ 연도λ₯Ό ν‘œμ‹œν•œλ‹€.
    • YμΆ• β†’ μ•± μ‚¬μš©μžλ₯Ό λ‚˜νƒ€λ‚΄λŠ” Linear Scale을 μ‚¬μš©ν•©λ‹ˆλ‹€. 0μ—μ„œ 3500κΉŒμ§€ λ²”μœ„λ₯Ό μ„€μ •ν•˜κ³ , λˆˆκΈˆμ„ 200 λ‹¨μœ„λ‘œ ν‘œμ‹œν•œλ‹€.
const x = d3.scaleBand() .domain(data.map((d) => d.date)) // 연도 λ²”μœ„ μ„€μ • .range([0, width]) .padding(0.1); const y = d3.scaleLinear() .domain([0, 3500]) // μ‚¬μš©μž 수 λ²”μœ„ μ„€μ • .range([height, 0]); chart.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(x)); chart.append("g") .call(d3.axisLeft(y).tickValues(d3.range(0, 3600, 200)));
 
  • Gridlines
    • Y좕을 κΈ°μ€€μœΌλ‘œ κ°€λ‘œμ„ (gridlines)을 μΆ”κ°€ν•˜μ—¬ 데이터 νŒλ…μ„ μš©μ΄ν•˜κ²Œ λ§Œλ“ λ‹€.
    • chart.append("g") .attr("class", "grid") .call(d3.axisLeft(y).tickSize(-width).tickFormat("")) // λˆˆκΈˆμ„  μ„€μ • .selectAll("line").style("opacity", 0.1); // λˆˆκΈˆμ„  투λͺ…도 μ„€μ • chart.selectAll(".grid .domain").remove(); // μΆ•μ˜ κΈ°λ³Έ 경계선 제거
 
  • λ²”λ‘€ 및 툴팁 μΆ”κ°€
    • 각 μ•±μ˜ μ„  색상이 무엇을 λ‚˜νƒ€λ‚΄λŠ”μ§€ λ²”λ‘€λ₯Ό μΆ”κ°€ν•œλ‹€.
    • rect둜 색상을 λ‚˜νƒ€λ‚΄κ³  text둜 μ•± 이름을 ν‘œμ‹œν•œλ‹€.
    • 마우슀λ₯Ό 각 데이터점에 올리면 툴팁이 λ‚˜νƒ€λ‚˜μ„œ μ•±μ˜ μ‚¬μš©μž 데이터λ₯Ό ν‘œμ‹œν•œλ‹€.
    • νˆ΄νŒμ€ div둜 κ΅¬ν˜„λ˜κ³ , mouseover μ΄λ²€νŠΈμ—μ„œ λ‚˜νƒ€λ‚œλ‹€.
    • const tooltip = d3 .select("body") .append("div") .attr("class", "tooltip") .style("opacity", 0) .style("position", "absolute") .style("background-color", "white") .style("border", "solid") .style("border-width", "1px") .style("border-radius", "5px") .style("padding", "10px"); Object.keys(data[0]) .filter((key) => key !== "date") .forEach((key, i) => { chart .selectAll(`.dot-${key}`) .data(data) .enter() .append("circle") .attr("class", `dot-${key}`) .attr("cx", (d) => x(d.date) + x.bandwidth() / 2) .attr("cy", (d) => y(d[key])) .attr("r", 5) .attr("fill", colors[i % colors.length]) .on("mouseover", (evt, d) => { const date = d.date; const values = Object.keys(d) .filter((key) => key !== "date") .map( (key, index) => `<div style="display: flex; align-items: center;"><div style="width: 10px; height: 10px; background-color: ${ colors[index % colors.length] }; margin-right: 5px;"></div>${key}: ${d[key]}</div>` ) .join(""); tooltip.transition().duration(200).style("opacity", 0.9); tooltip .html(`<strong>${date}</strong><br/><br/>${values}`) .style("left", evt.pageX + 5 + "px") .style("top", evt.pageY - 28 + "px"); }) .on("mouseout", () => { tooltip.transition().duration(500).style("opacity", 0); }); });
 

끝 마치며

μ•žμ„œ Reactλ₯Ό μ‚¬μš©ν•˜λŠ” 것을 λͺ…μ‹œμ μœΌλ‘œ 보이기 μœ„ν•΄ refλ₯Ό μ‚¬μš©ν•œ μ˜ˆμ œκ°€ 일뢀 μ‘΄μž¬ν•œλ‹€.
ν•˜μ§€λ§Œ 이 과정은 ν•˜λ‚˜μ˜ λ„ν˜•μ„ 그리기 μœ„ν•΄ λ§Žμ€ μ–‘μ˜ μ½”λ“œλ₯Ό ν•„μš”λ‘œν•œλ‹€.
λ¦¬μ•‘νŠΈ 곡식 λ¬Έμ„œμ—λŠ” λ‹€μŒκ³Ό 같은 문ꡬ가 μžˆλ‹€.
πŸ’‘
Avoid using refs for anything that can be done declaratively.
λ”°λΌμ„œ μš°λ¦¬λŠ” ref μ‚¬μš©μ„ μžμ œν•  ν•„μš”κ°€ μžˆλ‹€.
 
React 15λΆ€ν„°λŠ” JSX 파일 λ‚΄μ—μ„œ svg μš”μ†Œμ— λŒ€ν•œ 지원이 μ΄λ£¨μ–΄μ§€λ―€λ‘œ λ‹€μŒκ³Ό 같이 원을 κ°„λ‹¨ν•˜κ²Œ ν‘œν˜„ν•  수 μžˆμŒμ„ 보여쀀 λ°” μžˆλ‹€.
const Svg3 = () => { return ( <> <svg> <circle cx="100" cy="80" r="50"></circle> </svg> </> ); }; export default Svg3;
μ΄λ ‡κ²Œ λͺ…λ Ήν˜• λŒ€μ‹  μ„ μ–Έν˜•μ„ μ‚¬μš©ν•¨μœΌλ‘œμ¨ μ½”λ“œλ₯Ό κ·Έλ¦¬λŠ” β€˜λ°©λ²•β€™μ— μ§‘μ€‘ν•˜κΈ°λ³΄λ‹€ κ·Έλ €μ§„ 것에 λŒ€ν•΄ λ¬˜μ‚¬ν•˜λŠ” 것에 집쀑할 수 있고, μ½”λ“œμ–‘ λ˜ν•œ κ°μ†Œμ‹œν‚¬ 수 μžˆλ‹€