굉장히 오랜만의 포스팅이네요.
그동안 놀았던건 아니고.. 부스트캠프 일정이 엄청 바빴던터라 못올리고 있었습니다....🥲
paperef.com
제가 부스트캠프 팀프로젝트로 했던 논문 인용관계 시각화 서비스입니다.
이제 자유의 몸이 되었으니, 위 프로젝트에서 논문 데이터를 어떻게 네트워크 차트로 시각화 했는지 그 과정들을 조금씩 기록해 보겠습니다.
Data-driven-documents. 맞춤형의, 인터랙티브한 charts/maps를 웹에 그리기위해 사용되는 자바스크립트 라이브러리
다양한 방법이 있겠지만.. 저의 경우는 web-worker를 이용하여 서브스레드에서 node, line 좌표 계산 + 메인스레드에서 렌더링 하는 방식으로 계산로직/렌더링로직을 분리해서 병렬처리하는 방법으로 성능개선을 했습니다. 추후 포스팅에서 좀 더 자세히 다뤄보겠습니다.
여기서 끝내기는 좀 아쉬워서.. 차트구현 내용은 아니지만,
제가 로딩스피너로 넣었던 위상이 변하는 달모양 spinner 만드는 과정을 알아보며 svg랑도 좀 친해질겸, D3를 찍먹 해봅시다.
<defs>
추후 사용될 그래픽 객체를 저장해두는 태그
<pattern>
svg 영역을 채울 패턴을 정의하는 태그
달 표면 그림을 패턴으로 정의하기위해 사용
<g>
svg elements를 그룹화하는 태그
<circle>
원을 그리는 태그
<path>
선을 그리는 태그
d
속성으로 선의 경로를 표현할 수 있습니다. 이 속성에 다양한 형태의 선을 정의하여 그릴 수 있는데, 저는 제가 사용한 두가지 속성 명령어에 대해서만 설명하겠습니다.
- M
x
y
좌표 x, y로 이동합니다.
- A
rx
ry
x축 회전각
큰 호 플래그
쓸기 방향 플래그
x
y
현재 좌표와 (x, y) 두 점을 지나면서 x축 반지름rx, y축 반지름ry, x축 회전각만큼 기울어진 타원호를 그리며 이동합니다.
이 때, 4가지 경우의 수가 생기는데, 이걸 특정해주는게 두개의 플래그입니다.
큰 호 플래그
- 0: 중심각이 180도 미만(작은 호) 1: 중심각이 180도 이상(큰 호)
쓸기 방향 플래그
- 0: 음각(반시계방향), 1: 양각(시계방향)
<circle cx="50" cy="50" r="49" fill="black" /> <!-- 🌑 -->
<path ref={pathRef} d="..." fill="white" /> <!-- 밝은부분 -->
<circle cx="50" cy="50" r="50" fill="url(#image11)" /> <!-- 반투명한 달 표면 레이어 -->
어두운부분(원) ⇒ 밝은부분(반원 + 타원 or 반원 - 타원) ⇒ 달표면 레이어를 차례로 쌓은다음, path의 d 속성(밝은부분의 궤적)을 100ms마다 변경하는 방법으로 구현했습니다.
우선 달의 밝은부분을 그리기 위해서는 원호, 타원호 궤적 2개가 필요합니다.
원호, 타원호 두개와 쓸기방향 플래그를 잘 바꿔가면서 그려주면 되겠군요.
M x y
A rx ry x-axis-rotation large-arc-flag
sweep-flag x y
A
안쪽타원 rx ry x-axis-rotation large-arc-flag
sweep-flag x y
: 고정값들
접선좌표, 바깥쪽 원의 반지름, 안쪽타원의 ry는 고정값이고, 반원과 안쪽타원의 larg-arc-flag는 모두 0으로 고정이 가능합니다.(180도 궤적이니)
따라서, 반원의 sweep-flag, 안쪽타원의 rx, sweep-flag 세가지만 계산해주면 됩니다.
안쪽 타원의 rx 는 r * cos(t)
(아래그림참고), 쓸기 방향 플래그만 🌑 -> 🌓 -> 🌕 -> 🌗 이 시점에 잘 바꿔서 그리면 됩니다.
그림실력이 이게 최선이네요... 저 두꺼운 선이 구의 입체면이라고 생각해주세요.
최종적으로, path를 계산하는 함수는 다음과 같이 만들어졌습니다.
const calculateMoonLightPath = (radian: number) => {
return `M 50 0
A 50 50 0 0 ${Math.floor(radian / Math.PI) % 2} 50 100
A ${50 * Math.cos(radian)} 50 0 0 ${Math.floor((radian / Math.PI) * 2) % 2} 50 0`;
};
import * as d3 from 'd3';
import { useEffect, useRef } from 'react';
import styled from 'styled-components';
import theme from '../style/theme';
const calculateMoonLightPath = (radian: number) => {
return `M 50 0
A 50 50 0 0 ${Math.floor(radian / Math.PI) % 2} 50 100
A ${50 * Math.cos(radian)} 50 0 0 ${Math.floor((radian / Math.PI) * 2) % 2} 50 0`;
};
const MoonLoader = () => {
const pathRef = useRef<SVGPathElement>(null);
useEffect(() => {
let radian = Math.PI;
const timer = d3.interval(() => {
d3.select(pathRef.current).attr('d', calculateMoonLightPath(radian));
radian += Math.PI / 10;
}, 100);
return () => timer.stop();
}, []);
return (
<MoonContainer>
<svg width="50" height="50" viewBox="0 0 100 100" style={{ filter: 'url(#inset-shadow)' }}>
<defs>
<pattern id="image11" x="0" y="0" patternUnits="userSpaceOnUse" height="100" width="100">
<image x="0" y="0" height="100" width="100" xlinkHref="https://www.icalendar37.net/lunar/api/i.png"></image>
</pattern>
</defs>
<g>
<circle cx="50" cy="50" r="49" stroke="0" fill={theme.COLOR.primary4} />
<path ref={pathRef} fill={theme.COLOR.offWhite} />
<circle cx="50" cy="50" r="50" strokeWidth="0" fill="url(#image11)" />
</g>
</svg>
</MoonContainer>
);
};
const MoonContainer = styled.div`
text-align: center;
`;
export default MoonLoader;
d3.select(selector)
selector가 string이면 해당 selector와 매칭되는 첫번째 element와 매칭되며, string이 아니면 파라미터 그 자체(여기서는 path 돔요소)와 매칭되는 Selection을 반환합니다.
Selection.attr("d", string)
선택요소의 d 속성을 변경합니다.
d3.interval(callback, delay)
주어진 delay시간마다 함수를 반복해서 실행합니다.
callback : (elapsed) ⇒ void;
delay : callback을 수행시킬 interval time.
참고) 달의 위상은 ← 방향으로 변해야합니다. 거꾸로 돌리지 않게 조심.... (이거 때문에 유튜브 중등과학 시청함)
위의 방법으로 리액트 + D3코드로 path를 100ms마다 변형시켰습니다
그럼.. 잘 돌아갑니다 ^^
달 모양 로더를 구현하기 위한 과정과 결과가 모두 아름답네요,,