들어가며
디자인을 공부하던 도중 D3에 대한 깊은 이해가 필요하다는걸 느끼고 새로운 토이 프로젝트를 하게 되었다. 지시받은 내용은 두 가지였다.
- D3의 라인차트를 주로 다루게 될것이니 라인차트에 대한 이해도를 쌓을 것
- 관련 데이터들은 목업을 활용할 수 있긴 하지만 가급적 api를 활용할 것.
2번이 의외로 힘들었다. 가장 먼저 떠오른 것은 기상청이었으나 주는 데이터들이 시원치않아서 클로드에게 다른 api 사이트를 추천받았다. 그 과정에서 openmeteo라는 사이트를 알게 되었고 이곳에서 풍속 지표를 받을 수 있었다.
차트 만들기
1단계 : 데이터 가공
받아온 데이터는 시간, 풍속1, 풍속2가 따로 배열로 연결된 형태였다. 이를 하나의 series로 연결하였다.
TYPESCRIPT1interface WeatherData { 2 time: Date[]; 3 wind_speed_10m: Float32Array; 4 wind_speed_50m: Float32Array; 5} 6 7 const series = data.time.map((t, i) => ({ 8 time: t, 9 v10: data.wind_speed_10m[i], 10 v50: data.wind_speed_50m[i], 11 }));
2단계 : 좌표계 만들기
좌표계는 이전에 포스팅했듯 "X좌표는 0부터 최대치를 설정하면 되는것 아닌가?" 하는 안일한 생각을 했었으나 x좌표가 날짜인 이상 그런식으로 설정할 수는 없었다. ScaleTime이라는 새로운 방식의 메소드를 사용했고 이를 통해 x좌표를 설정했다.
y좌표는 동일하게 scaleLinear를 사용하였다.
TYPESCRIPT1 const xScale = d3 2 .scaleTime() //extent는 최솟값, 최댓값 반환하는 메소 3 .domain(d3.extent(series, (d) => d.time) as [Date, Date]) 4 .range([0, innerWidth]); 5 6 const allValues = [...series.map((d) => d.v10), ...series.map((d) => d.v50)]; 7 const yScale = d3 8 .scaleLinear() 9 .domain([0, (d3.max(allValues) as number) * 1.1]) 10 .nice() //fixed처럼 자릿수 정리 11 .range([innerHeight, 0]);
3단계 : 라인 그리기
이제 두 풍속의 라인그래프를 그릴 차례이다. 위에서 만들어 놨던 series를 사용해서 d3.line()으로 SVG path 문자열을 생성했다.
TYPESCRIPT1const makeLine = (key: "v10" | "v50") => 2 d3 3 .line<(typeof series)[0]>() 4 .x((d) => xScale(d.time)) // 각 데이터의 시간 → x좌표 5 .y((d) => yScale(d[key])) // 각 데이터의 풍속 → y좌표 6 (series) ?? ""; // series를 넣으면 path 문자열 반환 7 8const line10 = makeLine("v10"); 9const line50 = makeLine("v50");
d3.line()은 데이터 배열을 받아서 문자열을 만들어주는 함수다. 좌표를 하나하나 계산할 필요 없이 .x()와 .y()에 스케일 변환만 넘겨주면 D3가 알아서 path를 만들어준다.
TSX1<path d={line10} fill="none" stroke="#2563eb" strokeWidth={1.5} /> 2<path d={line50} fill="none" stroke="#dc2626" strokeWidth={1.5} strokeDasharray="6 3" />
10m 풍속은 파란 실선, 50m 풍속은 빨간 점선으로 구분했다. strokeDasharray="6 3"은 6px 그리고 3px 비우는 패턴을 반복해서 점선을 만드는 SVG 속성이다. fill="none"을 꼭 넣어야 하는데, 안 그러면 path가 닫힌 도형으로 인식되어 내부가 검정으로 채워진다
4단계 : 축과 그리드 그리기
위에서 언급했듯, d3로 계산하고 react가 직접 그리는 방식을 체택했습니다.
TYPESCRIPT1// D3가 6개쯤 되는 적절한 눈금값을 자동 선택 2const xTicks = xScale.ticks(6);
5단계 : 툴팁 인터렉션
툴팁은 마우스를 올렸을 때 툴팁의 내용이 나오도록 설계했습니다.
TYPESCRIPT1 //series 중 time을 기준으로 가장 가까운 인덱스를 찾아 2 const bisect = d3.bisector<(typeof series)[0], Date>((d) => d.time).center; 3 4 const handleMouseMove = (e: React.MouseEvent<SVGRectElement>) => { 5 const rect = e.currentTarget.getBoundingClientRect(); 6 const mx = e.clientX - rect.left; 7 const idx = bisect(series, xScale.invert(mx)); 8 const d = series[idx]; 9 if (!d || !tooltipRef.current) return; 10 11 tooltipRef.current.style.opacity = "1"; 12 tooltipRef.current.style.left = `${e.clientX + 12}px`; 13 tooltipRef.current.style.top = `${e.clientY - 48}px`; 14 tooltipRef.current.innerHTML = 15 `<div style="margin-bottom:4px;color:#6b7280">${d3.timeFormat("%Y-%m-%d %H:%M")(d.time)}</div>` + 16 `<div style="color:#2563eb">10m: <b>${d.v10.toFixed(1)} m/s</b></div>` + 17 `<div style="color:#dc2626">50m: <b>${d.v50.toFixed(1)} m/s</b></div>`; 18 }; 19
e.clientX는 브라우저 화면 전체 기준의 마우스 x좌표입니다. 여기서 차트 <rect>의 왼쪽 끝 위치를 빼면 차트 내부 기준의 x좌표가 나옵니다.